gaoluyang
2026-05-15 a9f70566970293fdb065075610dea1bb6ab386f0
Merge branch 'dev_NEW_pro' into dev_天津_宝东

# Conflicts:
# multiple/config.json
# src/api/productionManagement/productionOrder.js
# src/api/salesManagement/deliveryLedger.js
# src/views/basicData/customerFile/index.vue
# src/views/basicData/customerFileOpenSea/index.vue
# src/views/basicData/product/index.vue
# src/views/collaborativeApproval/approvalManagement/index.vue
# src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
# src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
# src/views/collaborativeApproval/approvalProcess/index.vue
# src/views/collaborativeApproval/purchaseApproval/index.vue
# src/views/customerService/feedbackRegistration/components/formDia.vue
# src/views/equipmentManagement/upkeep/Form/formDia.vue
# src/views/equipmentManagement/upkeep/index.vue
# src/views/inventoryManagement/dispatchLog/Record.vue
# src/views/inventoryManagement/stockManagement/Qualified.vue
# src/views/inventoryManagement/stockManagement/Record.vue
# src/views/inventoryManagement/stockReport/index.vue
# src/views/procurementManagement/procurementLedger/index.vue
# src/views/procurementManagement/purchaseReturnOrder/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/index.vue
# src/views/qualityManagement/processInspection/components/formDia.vue
# src/views/salesManagement/deliveryLedger/index.vue
# src/views/salesManagement/returnOrder/components/detailDia.vue
# src/views/salesManagement/salesLedger/index.vue
# src/views/salesManagement/salesQuotation/index.vue
已添加83个文件
已修改190个文件
已删除9个文件
58780 ■■■■■ 文件已修改
FILE_UPLOAD_README.md 480 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
FINANCIAL_MANAGEMENT_BACKEND_SPEC.md 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jsconfig.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/BTYXfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/DYKJfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/DZZBfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYZCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/KSfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/KYHGfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/SDJCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/WTXCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/ZXZNfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/BTYXLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/DYKJLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/DZZBLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYZCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/KSLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/KYHGLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/SDJCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/WTXCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/ZXZNLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/multiple-build.js 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/common.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customer.js 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerFile.js 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/parameterMaintenance.js 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/productProcess.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/storageAttachment.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/repair.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/accountPurchase.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/accountSales.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/accountSubject.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/fixedAsset.js 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/intangibleAsset.js 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/ledger.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/voucher.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInRecord.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockOut.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockUninventory.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/oaSystem/projectManagement.js 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementInvoiceLedger.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/purchase_return_order.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRoute.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRouteFile.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRouteItem.js 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productBom.js 24 ●●●●● 补丁 | 查看 | 原始文档 | 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 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProcess.js 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionPlan/productionPlan.js 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/deliveryLedger.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/appVersion.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/仓储助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/待办助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/生产助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/财务助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/质量助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/采购助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/AI/销售助手.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/aiIndustrialBrain/reference-cards.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/aiIndustrialBrain/reference-chat.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/logo/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/index.scss 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/generalAssistant.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/index.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/purchaseAssistant.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 4941 ●●●●● 补丁 | 查看 | 原始文档 | 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/Dialog/FileList.vue 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/FileListDialog.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Editor/index.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImagePreview/index.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImageUpload/index.vue 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/PIMTable.vue 859 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProcessParamListDialog.vue 670 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProjectManagement/ProgressReportDialog.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PurchaseAIChatSidebar/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SvgIcon/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/filePreview/index.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 234 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 273 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/download.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.js 83 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.js 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/generator/html.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/index.vue 1499 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFileOpenSea/index.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/parameterMaintenance/index.vue 793 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 1142 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/HomeTab.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 852 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 64 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/fileList.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/attendanceManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/enterpriseBook/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/meetingBoard/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/purchaseApproval/index.vue 1960 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rpaManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rulesRegulationsManagement/index.vue 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/sealManagement/index.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/shipmentReview/fileList.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/afterSalesHandling/index.vue 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/expiryAfterSales/components/formDia.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/expiryAfterSales/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/formDia.vue 894 ●●●● 补丁 | 查看 | 原始文档 | 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 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue 63 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/formDia.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/operationManagement/index.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/RepairModal.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/PlanModal.vue 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/formDia.vue 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 1121 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/example/DynamicTableExample.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/borrow/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/document/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/return/index.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/fixedAssets.vue 482 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/intangibleAssets.vue 480 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/expenseManagement/index.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/generalLedger/index.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | 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 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/purchaseReturn.vue 239 ●●●●● 补丁 | 查看 | 原始文档 | 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 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/salesReturn.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/revenueManagement/index.vue 445 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/detailLedger.vue 309 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/generalLedger.vue 312 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/index.vue 1110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/Record.vue 855 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/index.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/New.vue 335 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Qualified.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Record.vue 359 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Subtract.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 1251 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/lavorissue/ledger/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/lavorissue/statistics/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/milestoneList.vue 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/phaseGoalList.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/projectForm.vue 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/taskTree.vue 834 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/index.vue 481 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/projectDetail.vue 565 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/components/formDia.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/index.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentEntry/index.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentLedger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementInvoiceLedger/index.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/fileList.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 348 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementReport/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/New.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/ProductList.vue 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/index.vue 489 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/New.vue 285 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 425 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 2216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processStatistics/index.vue 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/index.vue 869 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/index.vue 705 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/New.vue 584 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue 381 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue 536 ●●●●● 补丁 | 查看 | 原始文档 | 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 970 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 1339 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/index.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionTraceability/index.vue 722 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/components/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderEdit/index.vue 671 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue 502 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/components/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 326 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/components/PIMTable.vue 470 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 1340 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/formDia.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/projectDetail.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/components/ProjectTypeDialog.vue 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/index.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 844 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/filesDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index0.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-center.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | 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 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/fileList.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safetyTrainingAssessment/index.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 1416 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/invoiceLedger/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPaymentLedger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/detailDia.vue 260 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/formDia.vue 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/fileList.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 5422 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appVersion/index.vue 268 ●●●●● 补丁 | 查看 | 原始文档 | 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>
```
如果你的目标是“先上传,再跟业务一起保存”,这套写法可以直接作为基础模板使用。
FINANCIAL_MANAGEMENT_BACKEND_SPEC.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,233 @@
# è´¢åŠ¡ç®¡ç†åŽç«¯æ–‡æ¡£ï¼ˆä»…è´Ÿè´£æ¨¡å—ï¼‰
更新时间:2026-05-12
适用范围(仅以下 6 ä¸ªæ¨¡å—):
1. å›ºå®šèµ„产(`/financial/fixed-assets`)
2. æ— å½¢èµ„产(`/financial/intangible-assets`)
3. æ€»è´¦ç§‘目(`/financial/general-ledger`)
4. å‡­è¯ï¼ˆ`/financial/voucher`)
5. ç§‘目总账(`/financial/voucher-general-ledger`)
6. ç§‘目明细账(`/financial/voucher-detail-ledger`)
---
## 1. ç»Ÿä¸€çº¦å®š
### 1.1 å“åº”结构
```json
{
  "code": 200,
  "msg": "success",
  "data": {}
}
```
### 1.2 åˆ†é¡µç»“构(如果是分页接口)
请求参数建议:
- `current`(页码)
- `size`(每页条数)
响应建议:
```json
{
  "code": 200,
  "data": {
    "records": [],
    "total": 0
  }
}
```
### 1.3 é‡‘额与精度
- é‡‘额字段建议 `decimal(18,2)`。
- å‰åŽç«¯ç»Ÿä¸€ä¿ç•™ä¸¤ä½å°æ•°ã€‚
---
## 2. æ¨¡å—一:总账科目(已接真实 API)
前端文件:`src/views/financialManagement/generalLedger/index.vue`
API æ–‡ä»¶ï¼š`src/api/financialManagement/accountSubject.js`
### 2.1 æŽ¥å£çŽ°çŠ¶
- `GET /accountSubject/list`
- `POST /accountSubject/add`
- `PUT /accountSubject/edit`
- `DELETE /accountSubject/remove/{ids}`
- `POST /accountSubject/export`
### 2.2 å­—段模型
- `id`
- `subjectCode`(科目编码)
- `subjectName`(科目名称)
- `subjectType`(科目类型)
- `balanceDirection`(余额方向:借方/贷方)
- `status`(0 å¯ç”¨ï¼Œ1 ç¦ç”¨ï¼‰
- `remark`
### 2.3 ä¸šåŠ¡è§„åˆ™
- `subjectCode`、`subjectName`、`subjectType` å¿…填。
- åˆ é™¤éœ€è¦åšå¼•用校验(若已被凭证分录引用,不允许删除)。
---
## 3. æ¨¡å—二:固定资产(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/assets/fixedAssets.vue`
### 3.1 å»ºè®®æŽ¥å£
- `GET /financial/fixedAsset/page`
- `POST /financial/fixedAsset/add`
- `PUT /financial/fixedAsset/update`
- `DELETE /financial/fixedAsset/delete`
- `POST /financial/fixedAsset/depreciate`(按月计提)
### 3.2 å­—段模型
- `id, assetCode, assetName, category, specification`
- `purchaseDate, originalValue, usefulLife, residualRate`
- `accumulatedDepreciation, netValue`
- `location, department, keeper, status, remark`
### 3.3 æ ¸å¿ƒå…¬å¼ï¼ˆå¿…须一致)
- `monthlyDepreciation = originalValue * (1 - residualRate/100) / (usefulLife*12)`
- `accumulatedDepreciation += monthlyDepreciation`
- `netValue = originalValue - accumulatedDepreciation`
### 3.4 çŠ¶æ€å»ºè®®
- `in_use`(在用)
- `idle`(闲置)
- `repair`(维修中)
- `scrapped`(报废)
---
## 4. æ¨¡å—三:无形资产(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/assets/intangibleAssets.vue`
### 4.1 å»ºè®®æŽ¥å£
- `GET /financial/intangibleAsset/page`
- `POST /financial/intangibleAsset/add`
- `PUT /financial/intangibleAsset/update`
- `DELETE /financial/intangibleAsset/delete`
- `POST /financial/intangibleAsset/amortize`(按月摊销)
### 4.2 å­—段模型
- `id, assetCode, assetName, category, certificateNo`
- `acquisitionDate, originalValue, amortizationPeriod, residualRate`
- `accumulatedAmortization, netValue`
- `validityDate, status, description, remark`
### 4.3 æ ¸å¿ƒå…¬å¼ï¼ˆå¿…须一致)
- `monthlyAmortization = originalValue * (1 - residualRate/100) / (amortizationPeriod*12)`
- `accumulatedAmortization += monthlyAmortization`
- `netValue = originalValue - accumulatedAmortization`
- å½“ `netValue <= 0`:
  - `netValue = 0`
  - `status = amortized`
### 4.4 çŠ¶æ€å»ºè®®
- `in_use`(在用)
- `expired`(到期)
- `amortized`(已摊销完)
---
## 5. æ¨¡å—四:凭证(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/voucher/index.vue`
### 5.1 å»ºè®®æŽ¥å£
- `GET /financial/voucher/page`
- `POST /financial/voucher/add`
- `PUT /financial/voucher/update`
- `POST /financial/voucher/post`(过账)
- `POST /financial/voucher/cancel`(作废)
- `GET /financial/voucher/detail/{id}`
### 5.2 ä¸»è¡¨å­—段
- `id, voucherNo, voucherDate, summary`
- `debit, credit, creator, status, attachmentCount, remark`
### 5.3 åˆ†å½•字段
- `subjectCode, subjectName, summary, debit, credit`
### 5.4 å…³é”®æ ¡éªŒ
- åˆ†å½•至少一条有效行(科目不空,且借方或贷方 > 0)。
- å€Ÿè´·å¹³è¡¡ï¼š`sum(debit) == sum(credit)` ä¸” > 0,不满足禁止保存。
### 5.5 çŠ¶æ€æµè½¬
- `unposted -> posted`
- `unposted -> cancelled`
---
## 6. æ¨¡å—五:科目总账(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/voucher/generalLedger.vue`
### 6.1 å»ºè®®æŽ¥å£
- `GET /financial/ledger/general`
### 6.2 è¯·æ±‚参数
- `subjectCode`(末级或指定科目)
- `startMonth`(YYYY-MM)
- `endMonth`(YYYY-MM)
### 6.3 å“åº”字段
- `date, voucherNo, summary`
- `debit, credit, direction, balance`
### 6.4 è§„则
- ä»…在选择科目后返回数据。
- æ”¯æŒâ€œæœŸåˆä½™é¢ / æœ¬æœˆåˆè®¡ / æœ¬å¹´ç´¯è®¡â€è¡Œï¼ˆå¯é€šè¿‡ `rowType` å­—段区分)。
---
## 7. æ¨¡å—六:科目明细账(当前前端为 mock,待后端实现)
前端文件:`src/views/financialManagement/voucher/detailLedger.vue`
### 7.1 å»ºè®®æŽ¥å£
- `GET /financial/ledger/detail`
### 7.2 è¯·æ±‚参数
- `subjectCode`
- `auxiliaryType`(customer/supplier/department/employee/project)
- `auxiliaryId`
- `startMonth`(YYYY-MM)
- `endMonth`(YYYY-MM)
### 7.3 å“åº”字段
- `date, voucherNo, summary`
- `debit, credit, direction, balance`
### 7.4 è§„则
- å…ˆé€‰ç§‘目,再查明细。
- è¾…助核算条件为可选,但建议后端支持维度过滤。
---
## 8. æŽ¨èæœ€å°è¡¨è®¾è®¡ï¼ˆä»…本范围)
- `fin_account_subject`
- `fin_fixed_asset`
- `fin_intangible_asset`
- `fin_voucher`
- `fin_voucher_entry`
- `fin_ledger_snapshot_general`(可选,做性能优化)
- `fin_ledger_snapshot_detail`(可选,做性能优化)
---
## 9. AI ç”ŸæˆåŽç«¯ä»»åŠ¡é¡ºåºï¼ˆå»ºè®®ï¼‰
1. å…ˆå®Œæˆ **总账科目**(已有 API,最稳定)。
2. å®Œæˆ **凭证 + åˆ†å½• + å€Ÿè´·å¹³è¡¡æ ¡éªŒ + çŠ¶æ€æµè½¬**。
3. å®žçް **科目总账 / ç§‘目明细账** æŸ¥è¯¢ã€‚
4. å®žçް **固定资产折旧** ä¸Ž **无形资产摊销**。
5. è¡¥æµ‹è¯•:
   - å€Ÿè´·å¹³è¡¡æ ¡éªŒ
   - æŠ˜æ—§/摊销公式
   - ç§‘目被引用禁止删除
jsconfig.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "~/*": ["./*"]
    }
  },
  "include": ["src/**/*.js", "src/**/*.vue", "vite.config.js"]
}
multiple/assets/favicon/BTYXfavicon.ico
multiple/assets/favicon/DYKJfavicon.ico
multiple/assets/favicon/DZZBfavicon.ico
multiple/assets/favicon/HYZCfavicon.ico
multiple/assets/favicon/KSfavicon.ico
multiple/assets/favicon/KYHGfavicon.ico
multiple/assets/favicon/SDJCfavicon.ico
multiple/assets/favicon/WTXCfavicon.ico
multiple/assets/favicon/ZXZNfavicon.ico
multiple/assets/logo/BTYXLogo.png
multiple/assets/logo/DYKJLogo.png
multiple/assets/logo/DZZBLogo.png
multiple/assets/logo/HYZCLogo.png
multiple/assets/logo/KSLogo.png
multiple/assets/logo/KYHGLogo.png
multiple/assets/logo/SDJCLogo.png
multiple/assets/logo/WTXCLogo.png
multiple/assets/logo/ZXZNLogo.png
multiple/multiple-build.js
@@ -24,6 +24,19 @@
const envFilePath = path.join(process.cwd(), '.env.production.local');
async function copyFileWithOverwrite(src, dest) {
    await fs.mkdir(path.dirname(dest), { recursive: true });
    if (fsSync.existsSync(dest)) {
        try {
            await fs.chmod(dest, 0o666);
        } catch {
            // Ignore chmod failure and try delete directly.
        }
        await fs.rm(dest, { force: true });
    }
    await fs.copyFile(src, dest);
}
try {
    // 1️⃣ ç”Ÿæˆ .env
    console.log("=======生成.env=======");
@@ -41,9 +54,8 @@
        const backupFile = path.join(replacePath, config[key]);
        const replaceFile = path.join(resourcePath, companyMap[key]);
        await fs.mkdir(path.dirname(backupFile), { recursive: true });
        await fs.copyFile(originFile, backupFile);
        await fs.copyFile(replaceFile, originFile);
        await copyFileWithOverwrite(originFile, backupFile);
        await copyFileWithOverwrite(replaceFile, originFile);
    }
    console.log("=====开始打包======");
@@ -66,7 +78,7 @@
            const originFile = path.join(rootPath, config[key]);
            const backupFile = path.join(replacePath, config[key]);
            await fs.copyFile(backupFile, originFile);
            await copyFileWithOverwrite(backupFile, originFile);
        }
        await fs.rm(replacePath, { recursive: true, force: true });
        console.log(`🗑️ å·²åˆ é™¤ ${replacePath}`);
package.json
@@ -56,5 +56,6 @@
  },
  "overrides": {
    "quill": "2.0.2"
  }
  },
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
public/favicon.ico

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,83 @@
import request from '@/utils/request'
export function listCustomer(query) {
    return request({
        url: '/basic/customer/list',
        method: 'get',
        params: query
    })
}
// åˆ†é…å®¢æˆ·
export function assignCustomer(data) {
    return request({
        url: '/basic/customer/assignCustomer',
        method: 'post',
        data
    })
}
// å›žæ”¶å®¢æˆ·
export function recycleCustomer(data) {
    return request({
        url: '/basic/customer/recycleCustomer',
        method: 'post',
        data
    })
}
// æµå…¥å…¬æµ·
export function backCustomer(id) {
    return request({
        url: '/basic/customer/back/' + id,
        method: 'post'
    })
}
export function shareCustomer(data) {
    return request({
        url: '/basic/customer/together',
        method: 'post',
        data: data
    })
}
export function getCustomer(id) {
    return request({
        url: '/basic/customer/' + id,
        method: 'get'
    })
}
export function addCustomer(data) {
    return request({
        url: '/basic/customer/addCustomer',
        method: 'post',
        data: data
    })
}
export function updateCustomer(data) {
    return request({
        url: '/basic/customer/updateCustomer',
        method: 'post',
        data: data
    })
}
export function exportCustomer(query) {
    return request({
        url: '/basic/customer/export',
        method: 'get',
        params: query,
        responseType: 'blob'
    })
}
export function delCustomer(ids) {
    return request({
        url: '/basic/customer/delCustomer',
        method: 'delete',
        data: ids
    })
}
src/api/basicData/customerFile.js
@@ -1,123 +1,5 @@
import request from '@/utils/request'
export function listCustomer(query) {
    return request({
        url: '/basic/customer/list',
        method: 'get',
        params: query
    })
}
// å®¢æˆ·æ¡£æ¡ˆç§æµ·æŸ¥è¯¢
export function listCustomerPrivatePool(query) {
    return request({
        url: '/customerPrivatePool/listPage',
        method: 'get',
        params: query
    })
}
export function addCustomerPrivatePool(data) {
    return request({
        url: '/customerPrivatePool/add',
        method: 'post',
        data: data
    })
}
export function addCustomerPrivate(data) {
    return request({
        url: '/customerPrivate/add',
        method: 'post',
        data: data
    })
}
export function delCustomerPrivate(ids) {
    return request({
        url: '/customerPrivate/delete',
        method: 'delete',
        data: ids
    })
}
export function delCustomerPrivatePool(id) {
    return request({
        url: '/customerPrivatePool/delete/' + id,
        method: 'delete',
    })
}
export function shareCustomer(data) {
    return request({
        url: '/customerPrivatePool/together',
        method: 'post',
        data: data
    })
}
export function getCustomer(id) {
    return request({
        url: '/basic/customer/' + id,
        method: 'get'
    })
}
export function getCustomerPrivatePoolById(id) {
    return request({
        url: '/customerPrivatePool/getbyId/' + id,
        method: 'get'
    })
}
export function getCustomerPrivatePoolInfo(id) {
    return request({
        url: '/customerPrivatePool/info/' + 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 updateCustomerPrivatePool(data) {
    return request({
        url: '/customerPrivatePool/update',
        method: 'put',
        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',
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/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/equipmentManagement/repair.js
@@ -70,3 +70,16 @@
    data,
  });
};
/**
 * @desc éªŒæ”¶å®¡æ‰¹
 * @param {验收参数} data
 * @returns
 */
export const repairAcceptance = (data) => {
  return request({
    url: `/device/repair/acceptance`,
    method: "post",
    data,
  });
};
src/api/financialManagement/accountPurchase.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import request from "@/utils/request";
/** é‡‡è´­å…¥åº“分页列表 */
export const listPageAccountPurchase = (params) => {
  return request({
    url: "/accountPurchase/listPageAccountPurchase",
    method: "get",
    params,
  });
};
/** é‡‡è´­é€€è´§åˆ†é¡µåˆ—表 */
export const listPageAccountPurchaseReturn = (params) => {
  return request({
    url: "/accountPurchase/listPageAccountPurchaseReturn",
    method: "get",
    params,
  });
};
src/api/financialManagement/accountSales.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import request from "@/utils/request";
/** é”€å”®å‡ºåº“分页列表 */
export const listPageAccountSales = (params) => {
  return request({
    url: "/accountSales/listPageAccountSales",
    method: "get",
    params,
  });
};
/** é”€å”®é€€è´§åˆ†é¡µåˆ—表 */
export const listPageAccountSalesReturn = (params) => {
  return request({
    url: "/accountSales/listPageAccountSalesReturn",
    method: "get",
    params,
  });
};
src/api/financialManagement/accountSubject.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
import request from "@/utils/request";
// æŸ¥è¯¢æ€»å¸ç§‘目列表
export function listAccountSubject(query) {
  return request({
    url: "/accountSubject/list",
    method: "get",
    params: query,
  });
}
// æ–°å¢žæ€»å¸ç§‘ç›®
export function addAccountSubject(data) {
  return request({
    url: "/accountSubject/add",
    method: "post",
    data: data,
  });
}
// ä¿®æ”¹æ€»å¸ç§‘ç›®
export function updateAccountSubject(data) {
  return request({
    url: "/accountSubject/edit",
    method: "put",
    data: data,
  });
}
// åˆ é™¤æ€»å¸ç§‘ç›®
export function delAccountSubject(ids) {
  return request({
    url: "/accountSubject/remove/" + ids,
    method: "delete",
  });
}
// å¯¼å‡ºæ€»å¸ç§‘ç›®
export function exportAccountSubject(data) {
  return request({
    url: "/accountSubject/export",
    method: "post",
    data: data,
    responseType: "blob",
  });
}
src/api/financialManagement/fixedAsset.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
import request from "@/utils/request";
// å›ºå®šèµ„产分页查询(current/size)
export function listFixedAssetPage(params) {
  return request({
    url: "/financial/fixedAsset/page",
    method: "get",
    params,
  });
}
// æ–°å¢žå›ºå®šèµ„产
export function addFixedAsset(data) {
  return request({
    url: "/financial/fixedAsset/add",
    method: "post",
    data,
  });
}
// ä¿®æ”¹å›ºå®šèµ„产
export function updateFixedAsset(data) {
  return request({
    url: "/financial/fixedAsset/update",
    method: "put",
    data,
  });
}
// åˆ é™¤å›ºå®šèµ„产(后端要求 ids=1&ids=2 å½¢å¼ï¼‰
export function deleteFixedAsset(ids) {
  const idList = Array.isArray(ids) ? ids : [ids];
  const query = idList
    .filter(id => id !== undefined && id !== null && id !== "")
    .map(id => `ids=${encodeURIComponent(id)}`)
    .join("&");
  return request({
    url: `/financial/fixedAsset/delete?${query}`,
    method: "delete",
  });
}
// æŠ˜æ—§è®¡æï¼ˆ{} è¡¨ç¤ºå…¨éƒ¨åœ¨ç”¨èµ„产)
export function depreciateFixedAsset(data = {}) {
  return request({
    url: "/financial/fixedAsset/depreciate",
    method: "post",
    data,
  });
}
src/api/financialManagement/intangibleAsset.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
import request from "@/utils/request";
// æ— å½¢èµ„产分页查询(current/size)
export function listIntangibleAssetPage(params) {
  return request({
    url: "/financial/intangibleAsset/page",
    method: "get",
    params,
  });
}
// æ–°å¢žæ— å½¢èµ„产
export function addIntangibleAsset(data) {
  return request({
    url: "/financial/intangibleAsset/add",
    method: "post",
    data,
  });
}
// ä¿®æ”¹æ— å½¢èµ„产
export function updateIntangibleAsset(data) {
  return request({
    url: "/financial/intangibleAsset/update",
    method: "put",
    data,
  });
}
// åˆ é™¤æ— å½¢èµ„产(后端要求 ids=1&ids=2 å½¢å¼ï¼‰
export function deleteIntangibleAsset(ids) {
  const idList = Array.isArray(ids) ? ids : [ids];
  const query = idList
    .filter(id => id !== undefined && id !== null && id !== "")
    .map(id => `ids=${encodeURIComponent(id)}`)
    .join("&");
  return request({
    url: `/financial/intangibleAsset/delete?${query}`,
    method: "delete",
  });
}
// æ‘Šé”€è®¡æï¼ˆ{} è¡¨ç¤ºå…¨éƒ¨åœ¨ç”¨èµ„产)
export function amortizeIntangibleAsset(data = {}) {
  return request({
    url: "/financial/intangibleAsset/amortize",
    method: "post",
    data,
  });
}
src/api/financialManagement/ledger.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import request from "@/utils/request";
// ç§‘目总账
export function getGeneralLedger(params) {
  return request({
    url: "/financial/ledger/general",
    method: "get",
    params,
  });
}
// ç§‘目明细账
export function getDetailLedger(params) {
  return request({
    url: "/financial/ledger/detail",
    method: "get",
    params,
  });
}
src/api/financialManagement/voucher.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
import request from "@/utils/request";
// å‡­è¯åˆ†é¡µæŸ¥è¯¢ï¼ˆcurrent/size + è¿‡æ»¤æ¡ä»¶ï¼‰
export function listVoucherPage(params) {
  return request({
    url: "/financial/voucher/page",
    method: "get",
    params,
  });
}
// æ–°å¢žå‡­è¯
export function addVoucher(data) {
  return request({
    url: "/financial/voucher/add",
    method: "post",
    data,
  });
}
// ä¿®æ”¹å‡­è¯ï¼ˆä»…未过账)
export function updateVoucher(data) {
  return request({
    url: "/financial/voucher/update",
    method: "put",
    data,
  });
}
// è¿‡è´¦
export function postVoucher(data) {
  return request({
    url: "/financial/voucher/post",
    method: "post",
    data,
  });
}
// ä½œåºŸ
export function cancelVoucher(data) {
  return request({
    url: "/financial/voucher/cancel",
    method: "post",
    data,
  });
}
// è¯¦æƒ…
export function getVoucherDetail(id) {
  return request({
    url: `/financial/voucher/detail/${id}`,
    method: "get",
  });
}
src/api/inventoryManagement/stockInRecord.js
@@ -24,4 +24,21 @@
        method: "delete",
        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
@@ -17,6 +17,14 @@
    });
};
export const getStockInventoryBatchNoQty = (params) => {
    return request({
        url: "/stockInventory/getBatchNoQty",
        method: "get",
        params,
    });
};
// åˆ›å»ºåº“存记录
export const createStockInventory = (params) => {
    return request({
@@ -30,6 +38,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,
    });
@@ -69,3 +95,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/oaSystem/projectManagement.js
ÎļþÒÑɾ³ý
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
@@ -19,6 +19,15 @@
    });
}
// æ ¹æ®é‡‡è´­å°è´¦ ID æŸ¥è¯¢å¯é€€äº§å“ç­‰ä¿¡æ¯
export function getPurchaseReturnOrderByPurchaseLedgerId(query) {
    return request({
        url: "/purchaseReturnOrders/getByPurchaseLedgerId",
        method: "get",
        params: query,
    });
}
// æŸ¥çœ‹è¯¦æƒ…
// purchaseReturnOrders/selectById/xxx
export function getPurchaseReturnOrderDetail(id) {
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,8 +67,8 @@
//  ä¸‹è½½æ¨¡æ¿
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,
  });
}
@@ -71,29 +81,70 @@
}
// ç”Ÿäº§è®¢å•-保存领料台账
// export function saveMaterialPickingLedger(data) {
//   return request({
//     url: "/productOrderMaterial/save",
//     method: "post",
//     data,
//   });
// }
export function saveMaterialPickingLedger(data) {
  return request({
    url: "/productOrderMaterial/update",
    url: "/productionOrderPick/savePick",
    method: "post",
    data,
  });
}
export function updateMaterialPickingLedger(data) {
  return request({
    url: "/productionOrderPick/updatePick",
    method: "post",
    data,
  });
}
// ç”Ÿäº§è®¢å•-领料详情列表
export function listMaterialPickingDetail(query) {
// ç”Ÿäº§è®¢å•溯源详情
export function getOrderDetail(npsNo) {
  return request({
    url: "/productOrderMaterial/detailList",
    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 listMaterialSupplementRecord(query) {
// ç”Ÿäº§è®¢å•-获取来源数据
export function getProductOrderSource(id) {
  return request({
    url: "/productOrderMaterial/supplementRecord",
    url: `/productionOrder/source/${id}`,
    method: "get",
    params: query,
  });
}
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,10 +24,18 @@
  });
}
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",
@@ -46,7 +54,7 @@
// å·¥å•-补料
export function addWorkOrderMaterialSupplement(data) {
  return request({
    url: "/productWorkOrder/material/supplement",
    url: "/productionOperationTask/material/supplement",
    method: "post",
    data,
  });
@@ -55,7 +63,7 @@
// å·¥å•-退料
export function addWorkOrderMaterialReturn(data) {
  return request({
    url: "/productWorkOrder/material/return",
    url: "/productionOperationTask/material/return",
    method: "post",
    data,
  });
@@ -64,7 +72,7 @@
// å·¥å•-补料记录
export function listWorkOrderMaterialSupplementRecord(query) {
  return request({
    url: "/productWorkOrder/material/supplementRecord",
    url: "/productionOperationTask/material/supplementRecord",
    method: "get",
    params: query,
  });
@@ -73,8 +81,17 @@
// å·¥å•-领用(提交实际领用数量)
export function pickWorkOrderMaterial(data) {
  return request({
    url: "/productWorkOrder/material/pick",
    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,21 @@
}
// ä¿®æ”¹å‘货台账
export function getDeliveryDetail(id) {
  return request({
    url: `/shippingInfo/getDateil/${id}`,
    method: "get",
  });
}
// ä¿®æ”¹å‘货台账
export function getDeliveryDetailByShippingNo(query) {
  return request({
    url: "/shippingInfo/getDateilByShippingNo",
    method: "get",
    params: query,
  });
}
export function addOrUpdateDeliveryLedger(query) {
  return request({
    url: "/shippingInfo/update",
src/api/system/appVersion.js
@@ -10,13 +10,10 @@
}
// ä¸Šä¼  APK
export function uploadApk(data) {
export function add(data) {
  return request({
    url: "/app/uploadApk",
    url: "/app/add",
    method: "post",
    data,
    headers: {
      "Content-Type": "multipart/form-data",
    },
    data
  });
}
src/assets/AI/²Ö´¢ÖúÊÖ.png
src/assets/AI/´ý°ìÖúÊÖ.png
src/assets/AI/Éú²úÖúÊÖ.png
src/assets/AI/²ÆÎñÖúÊÖ.png
src/assets/AI/ÖÊÁ¿ÖúÊÖ.png
src/assets/AI/²É¹ºÖúÊÖ.png
src/assets/AI/ÏúÊÛÖúÊÖ.png
src/assets/aiIndustrialBrain/reference-cards.png
src/assets/aiIndustrialBrain/reference-chat.png
src/assets/logo/logo.png

src/assets/styles/index.scss
@@ -148,7 +148,6 @@
  }
}
.table_list {
  margin-top: 20px;
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid var(--surface-border);
  border-radius: var(--radius-md);
src/components/AIChatSidebar/assistants/generalAssistant.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
import { Cpu } from '@element-plus/icons-vue'
export const generalAssistant = {
  key: 'general',
  label: '待办助理',
  title: '待办智能助理',
  tooltip: '待办助手',
  icon: Cpu,
  apiBase: '/xiaozhi',
  storageKey: 'ai_chat_uuid',
  placeholder: '请输入您的问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
  welcomeMessage: '你好',
  description: '我可以回答你的问题,为你提供业务数据解读信息、处理建议和辅助决策支持。',
  allowFileUpload: true,
  emptySessionText: '暂无历史会话',
  quickPrompts: [
    '我当前有哪些审批待办需要处理?',
    '帮我列出今天新增的审批待办。',
    '当前待我审批的单据,按时间倒序列出来。',
    '我发起的审批里,哪些还在处理中?',
    '查询流程编号 XXX çš„审批详情。',
    '流程编号 XXX çŽ°åœ¨å¡åœ¨å“ªä¸ªå®¡æ‰¹èŠ‚ç‚¹ï¼Ÿå½“å‰å®¡æ‰¹äººæ˜¯è°ï¼Ÿ',
    '帮我查看流程编号 XXX çš„审批流转记录。',
    '近7天我的审批待办统计情况怎么样?',
    '本月我的审批中,通过、驳回、处理中各有多少?',
    '近30天各类型审批数量分布是什么?',
    '帮我审批通过流程编号 XXX,备注“同意”。',
    '帮我驳回流程编号 XXX,备注“请补充说明”。',
    '撤销我刚刚对流程编号 XXX çš„审批操作。',
    '帮我修改流程编号 XXX çš„备注为“已补充附件”。',
    '删除我发起的流程编号 XXX。'
  ]
}
src/components/AIChatSidebar/assistants/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
import { generalAssistant } from './generalAssistant'
import { purchaseAssistant } from './purchaseAssistant'
export { generalAssistant, purchaseAssistant }
export const builtInAssistants = [generalAssistant, purchaseAssistant]
src/components/AIChatSidebar/assistants/purchaseAssistant.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
import { ShoppingCart } from '@element-plus/icons-vue'
export const purchaseAssistant = {
  key: 'purchase',
  label: '采购助理',
  title: '采购智能助理',
  tooltip: '采购智能助理',
  icon: ShoppingCart,
  apiBase: '/purchase-ai',
  storageKey: 'purchase_ai_chat_uuid',
  placeholder: '请输入采购问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
  welcomeMessage: '你好',
  description: '我可以协助你分析采购订单、到货进度、供应商表现和付款情况,帮助你快速定位采购异常。',
  allowFileUpload: true,
  allowMultipleFileUpload: true,
  fileAnalyzeUrl: '/purchase-ai/analyze-files',
  emptySessionText: '暂无采购会话',
  quickPrompts: [
    '本月采购金额排名前十的物料有哪些?',
    '哪些采购订单还未入库?',
    '最近7天供应商到货异常有哪些?',
    '帮我统计待付款采购单',
    '列出本月采购退货情况'
  ]
}
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/Dialog/FileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,263 @@
<template>
  <el-dialog v-model="isShow"
             :title="title"
             :width="width"
             @close="handleClose"
             class="attachment-dialog">
    <!-- å·¥å…·æ  -->
    <div v-if="editable"
         class="toolbar">
      <el-button type="primary"
                 size="small"
                 @click="handleUpload">
        ä¸Šä¼ é™„ä»¶
      </el-button>
    </div>
    <!-- ä¸Šä¼ ç»„件弹窗 -->
    <el-dialog v-model="uploadDialogVisible"
               title="上传附件"
               width="50%"
               @close="closeUpload">
      <AttachmentUpload v-model:file-list="newFileList" />
      <template #footer>
        <el-button @click="saveUpload">保存</el-button>
        <el-button @click="closeUpload">关闭</el-button>
      </template>
    </el-dialog>
    <!-- æ–‡ä»¶åˆ—表表格 -->
    <div class="table-container">
      <el-table :data="tableData"
                border
                class="attachment-table"
                :height="tableData.length > 0 ? 'auto' : '120px'">
        <el-table-column label="附件名称"
                         prop="originalFilename"
                         show-overflow-tooltip />
        <el-table-column v-if="showActions"
                         fixed="right"
                         label="操作"
                         :width="150"
                         align="center">
          <template #default="scope">
            <el-button link
                       type="primary"
                       size="small"
                       class="download-link"
                       @click="previewFile(scope.row.previewURL)">
              é¢„览
            </el-button>
            <el-button link
                       type="primary"
                       size="small"
                       class="download-link"
                       @click="downloadFile(scope.row.downloadURL)">
              ä¸‹è½½
            </el-button>
            <el-button v-if="editable"
                       link
                       type="danger"
                       size="small"
                       @click="handleDelete(scope.row)">
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </el-dialog>
  <filePreview ref="filePreviewRef" />
</template>
<script setup>
import { ElMessage } from 'element-plus'
  import { ref, computed, getCurrentInstance, onMounted, watch } from "vue";
  import AttachmentUpload from "@/components/AttachmentUpload/file/index.vue";
  import {
    attachmentList,
    deleteAttachment,
    createAttachment,
  } from "@/api/basicData/storageAttachment.js";
  import filePreview from '@/components/filePreview/index.vue'
  const filePreviewRef = ref()
  const props = defineProps({
    visible: {
      type: Boolean,
      required: true,
    },
    recordType: {
      type: String,
      default: "",
      required: true,
    },
    recordId: {
      type: Number,
      default: 0,
      required: true,
    },
    title: {
      type: String,
      default: "附件",
    },
    width: {
      type: String,
      default: "50%",
    },
    showActions: {
      type: Boolean,
      default: true,
    },
    editable: {
      type: Boolean,
      default: true,
    },
  });
  const emit = defineEmits(["close", "download", "upload", "delete"]);
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const uploadDialogVisible = ref(false);
  const newFileList = ref([]);
  const isShow = computed({
    get() {
      return props.visible;
    },
    set(val) {
      emit("update:visible", val);
    },
  });
  const handleClose = () => {
    isShow.value = false;
  };
  // é¢„览文件
  const previewFile = (url) => {
    if (url) {
      filePreviewRef.value.open(url)
    } else {
      ElMessage.warning('文件地址无效,无法预览')
    }
  }
  const handleUpload = () => {
    uploadDialogVisible.value = true;
  };
  const saveUpload = async () => {
    // æ£€æŸ¥æ˜¯å¦æœ‰æ–°ä¸Šä¼ çš„æ–‡ä»¶
    if (newFileList.value.length > 0) {
      createAttachment({
        application: "file",
        recordType: props.recordType,
        recordId: props.recordId,
        storageBlobDTOs: [...newFileList.value, ...tableData.value],
      }).then((res) => {
        if (res && res.code === 200) {
          proxy?.$modal?.msgSuccess("上传成功");
          newFileList.value = [];
          // åˆ·æ–°åˆ—表
          setList();
        }
      }).finally(() => {
        uploadDialogVisible.value = false;
      })
    }
  }
  const closeUpload = () => {
    newFileList.value = [];
    uploadDialogVisible.value = false;
  };
  const handleDelete = async (row, index) => {
    deleteAttachment([row.storageAttachmentId]).then((res) => {
      if (res && res.code === 200) {
        proxy?.$modal?.msgSuccess("删除成功");
        setList();
      }
    })
  };
  const setList = () => {
    attachmentList({
      recordType: props.recordType,
      recordId: props.recordId,
    }).then(res => {
      tableData.value = (res && res.data) || [];
    });
  };
  const downloadFile = url => {
    window.open(url, "_blank");
  };
  onMounted(() => {
    setList();
  });
</script>
<style scoped>
  .attachment-dialog {
    border-radius: 12px;
  }
  .toolbar {
    margin-bottom: 16px;
    text-align: right;
  }
  .table-container {
    max-height: 40vh;
    overflow-y: auto;
    min-height: 120px;
    padding-bottom: 16px;
    box-sizing: border-box;
    will-change: scroll-position;
    transform: translateZ(0);
    -webkit-overflow-scrolling: touch;
  }
  :deep(.el-table) {
    margin-bottom: 0;
  }
  :deep(.el-table__body-wrapper) {
    overflow-y: auto;
    will-change: transform;
    transform: translateZ(0);
  }
  :deep(.el-table__body tr) {
    transition: none;
  }
  :deep(.el-dialog__footer) {
    padding-top: 12px;
    border-top: 1px solid #e9ecef;
  }
  .attachment-table {
    border-radius: 8px;
  }
  :deep(.el-dialog__header) {
    background-color: #f8f9fa;
    border-bottom: 1px solid #e9ecef;
    padding: 16px 20px;
  }
  :deep(.el-dialog__title) {
    font-size: 16px;
    font-weight: 600;
  }
  :deep(.el-dialog__body) {
    padding: 16px 20px;
  }
  :deep(.el-table__empty-text) {
    color: #999;
  }
</style>
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,
      },
    })
    .then((res) => {
      handleUploadSuccess(res.data);
    });
  formData.append("files", file);
  uploadPublicFile(formData).then((res) => {
    handleUploadSuccess(res)
  })
}
</script>
src/components/ImagePreview/index.vue
ÎļþÒÑɾ³ý
src/components/ImageUpload/index.vue
ÎļþÒÑɾ³ý
src/components/PIMTable/PIMTable.vue
@@ -1,83 +1,76 @@
<template>
  <el-table
    ref="multipleTable"
    v-loading="tableLoading"
    :border="border"
    :data="tableData"
    :header-cell-style="mergedHeaderCellStyle"
    :height="height"
    :highlight-current-row="highlightCurrentRow"
    :row-class-name="rowClassName"
    :row-style="rowStyle"
    :row-key="rowKey"
    :style="tableStyle"
    tooltip-effect="dark"
    :expand-row-keys="expandRowKeys"
    :show-summary="isShowSummary"
    :summary-method="summaryMethod"
    @row-click="rowClick"
    @current-change="currentChange"
    @selection-change="handleSelectionChange"
    @expand-change="expandChange"
    class="lims-table"
  >
    <el-table-column
      align="center"
      type="selection"
      width="55"
      v-if="isSelection"
    />
    <el-table-column align="center" label="序号" type="index" width="60" />
    <el-table-column
      v-for="(item, index) in column"
      :key="index"
      :column-key="item.columnKey"
      :filter-method="item.filterHandler"
      :filter-multiple="item.filterMultiple"
      :filtered-value="item.filteredValue"
      :filters="item.filters"
      :fixed="item.fixed"
      :label="item.label"
      :prop="item.prop"
      :show-overflow-tooltip="item.dataType !== 'action' && item.dataType !== 'slot'"
      :align="item.align"
      :sortable="!!item.sortable"
      :type="item.type"
      :width="item.width"
      :minWidth="item.minWidth"
    >
  <el-table ref="multipleTable"
            v-loading="tableLoading"
            :border="border"
            :data="tableData"
            :header-cell-style="mergedHeaderCellStyle"
            :height="height"
            :highlight-current-row="highlightCurrentRow"
            :row-class-name="rowClassName"
            :row-style="rowStyle"
            :row-key="rowKey"
            :style="tableStyle"
            tooltip-effect="dark"
            :expand-row-keys="expandRowKeys"
            :show-summary="isShowSummary"
            :summary-method="summaryMethod"
            @row-click="rowClick"
            @current-change="currentChange"
            @selection-change="handleSelectionChange"
            @expand-change="expandChange"
            class="lims-table">
    <el-table-column align="center"
                     type="selection"
                     :selectable="selectable"
                     width="55"
                     v-if="isSelection" />
    <el-table-column align="center"
                     label="序号"
                     type="index"
                     width="60" />
    <el-table-column v-for="(item, index) in column"
                     :key="index"
                     :column-key="item.columnKey"
                     :filter-method="item.filterHandler"
                     :filter-multiple="item.filterMultiple"
                     :filtered-value="item.filteredValue"
                     :filters="item.filters"
                     :fixed="item.fixed"
                     :label="item.label"
                     :prop="item.prop"
                     :show-overflow-tooltip="item.dataType !== 'action' && item.dataType !== 'slot'"
                     :align="item.align"
                     :sortable="!!item.sortable"
                     :type="item.type"
                     :width="item.width"
                     :minWidth="item.minWidth">
      <template #header="scope">
        <div class="pim-table-header-cell" :class="{ 'has-extra': item.headerSlot }">
        <div class="pim-table-header-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"
          :name="item.theadSlot"
          :index="scope.$index"
          :row="scope.row"
        />
      <template v-if="item.hasOwnProperty('colunmTemplate')"
                #[item.colunmTemplate]="scope">
        <slot v-if="item.theadSlot"
              :name="item.theadSlot"
              :index="scope.$index"
              :row="scope.row" />
      </template>
      <template #default="scope">
        <!-- æ’æ§½ -->
        <div v-if="item.dataType == 'slot'">
          <slot
            v-if="item.slot"
            :index="scope.$index"
            :name="item.slot"
            :row="scope.row"
          />
          <slot v-if="item.slot"
                :index="scope.$index"
                :name="item.slot"
                :row="scope.row" />
        </div>
        <!-- è¿›åº¦æ¡ -->
        <div v-else-if="item.dataType == 'progress'">
@@ -85,128 +78,111 @@
        </div>
        <!-- å›¾ç‰‡ -->
        <div v-else-if="item.dataType == 'image'">
          <img
            :src="javaApi + '/img/' + scope.row[item.prop]"
            alt=""
            style="width: 40px; height: 40px; margin-top: 10px"
          />
          <img :src="javaApi + '/img/' + scope.row[item.prop]"
               alt=""
               style="width: 40px; height: 40px; margin-top: 10px" />
        </div>
        <!-- tag -->
        <div v-else-if="item.dataType == 'tag'">
          <el-tag
            v-if="
          <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)"
          >
                  :title="formatters(scope.row[item.prop], item.formatData)"
                  :type="formatType(scope.row[item.prop], item.formatType)">
            {{ formatters(scope.row[item.prop], item.formatData) }}
          </el-tag>
          <el-tag
            v-for="(tag, index) in dataTypeFn(
          <el-tag v-for="(tag, index) in dataTypeFn(
              scope.row[item.prop],
              item.formatData
            )"
            v-else-if="
                  v-else-if="
              typeof dataTypeFn(scope.row[item.prop], item.formatData) ===
              'object'
            "
            :key="index"
            :title="formatters(scope.row[item.prop], item.formatData)"
            :type="formatType(tag, item.formatType)"
          >
                  :key="index"
                  :title="formatters(scope.row[item.prop], item.formatData)"
                  :type="formatType(tag, item.formatType)">
            {{ item.tagGroup ? tag[item.tagGroup.label] ?? tag : tag }}
          </el-tag>
          <el-tag
            v-else
            :title="formatters(scope.row[item.prop], item.formatData)"
            :type="formatType(scope.row[item.prop], item.formatType)"
          >
          <el-tag v-else
                  :title="formatters(scope.row[item.prop], item.formatData)"
                  :type="formatType(scope.row[item.prop], item.formatType)">
            {{ formatters(scope.row[item.prop], item.formatData) }}
          </el-tag>
        </div>
        <!-- æŒ‰é’® -->
        <div v-else-if="item.dataType == 'action'" @click.stop>
          <template v-for="(o, key) in item.operation" :key="key">
            <el-button
              v-show="o.type != 'upload'"
              v-if="o.showHide ? o.showHide(scope.row) : true"
              :disabled="isOperationDisabled(o, scope.row)"
              :plain="o.plain"
              type="primary"
              :style="{
        <div v-else-if="item.dataType == 'action'"
             @click.stop>
          <template v-for="(o, key) in item.operation"
                    :key="key">
            <el-button v-show="o.type != 'upload'"
                       v-if="o.showHide ? o.showHide(scope.row) : true"
                       :disabled="isOperationDisabled(o, scope.row)"
                       :plain="o.plain"
                       type="primary"
                       :style="{
                color: getOperationColor(o, scope.row),
                fontWeight: 'bold',
              }"
              link
              @click.stop="o.clickFun(scope.row)"
              :key="key"
            >
                       link
                       @click.stop="o.clickFun(scope.row)"
                       :key="key">
              {{ o.name }}
            </el-button>
            <el-upload
              :action="
            <el-upload :action="
                javaApi +
                o.url +
                '?id=' +
                (o.uploadIdFun ? o.uploadIdFun(scope.row) : scope.row.id)
              "
              ref="uploadRef"
              :multiple="o.multiple ? o.multiple : false"
              :limit="1"
              :disabled="isOperationDisabled(o, scope.row)"
              :accept="
                       ref="uploadRef"
                       :multiple="o.multiple ? o.multiple : false"
                       :limit="1"
                       :disabled="isOperationDisabled(o, scope.row)"
                       :accept="
                o.accept
                  ? o.accept
                  : '.jpg,.jpeg,.png,.gif,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar'
              "
              v-if="o.type == 'upload'"
              style="display: inline-block; width: 50px"
              v-show="o.showHide ? o.showHide(scope.row) : true"
              :headers="uploadHeader"
              :before-upload="(file) => beforeUpload(file, scope.$index)"
              :on-change="
                       v-if="o.type == 'upload'"
                       style="display: inline-block; width: 50px"
                       v-show="o.showHide ? o.showHide(scope.row) : true"
                       :headers="uploadHeader"
                       :before-upload="(file) => beforeUpload(file, scope.$index)"
                       :on-change="
                (file, fileList) => handleChange(file, fileList, scope.$index)
              "
              :on-error="
                       :on-error="
                (error, file, fileList) =>
                  onError(error, file, fileList, scope.$index)
              "
              :on-success="
                       :on-success="
                (response, file, fileList) =>
                  handleSuccessUp(response, file, fileList, scope.$index)
              "
              :on-exceed="onExceed"
              :show-file-list="false"
            >
              <el-button
                link
                type="primary"
                :disabled="isOperationDisabled(o, scope.row)"
                :style="{
                       :on-exceed="onExceed"
                       :show-file-list="false">
              <el-button link
                         type="primary"
                         :disabled="isOperationDisabled(o, scope.row)"
                         :style="{
                  color: getOperationColor(o, scope.row),
                }"
                >{{ o.name }}</el-button
              >
                }">{{ o.name }}</el-button>
            </el-upload>
          </template>
        </div>
        <!-- å¯ç‚¹å‡»çš„æ–‡å­— -->
        <div
          v-else-if="item.dataType == 'link'"
          class="cell link"
          style="width: 100%"
          @click="goLink(scope.row, item.linkMethod)"
        >
        <div v-else-if="item.dataType == 'link'"
             class="cell link"
             style="width: 100%"
             @click="goLink(scope.row, item.linkMethod)">
          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
        </div>
        <!-- é»˜è®¤çº¯å±•示数据 -->
        <div v-else class="cell" style="width: 100%">
        <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)
@@ -215,326 +191,337 @@
      </template>
    </el-table-column>
  </el-table>
  <pagination
        v-if="isShowPagination"
    :total="page.total"
    :layout="page.layout"
    :page="page.current"
    :limit="page.size"
    @pagination="paginationSearch"
  />
  <pagination v-if="isShowPagination"
              :total="page.total"
              :layout="page.layout"
              :page="page.current"
              :limit="page.size"
              @pagination="paginationSearch" />
</template>
<script setup>
import pagination from "./Pagination.vue";
import { computed, ref, inject, getCurrentInstance } from "vue";
import { ElMessage } from "element-plus";
  import pagination from "./Pagination.vue";
  import { computed, ref, inject, getCurrentInstance } from "vue";
  import { ElMessage } from "element-plus";
// èŽ·å–å…¨å±€çš„ uploadHeader
const { proxy } = getCurrentInstance();
const uploadHeader = proxy.uploadHeader;
const javaApi = proxy.javaApi;
  // èŽ·å–å…¨å±€çš„ uploadHeader
  const { proxy } = getCurrentInstance();
  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) => {
  return typeof val === "function" ? val(row) : val;
};
  // Filters
  const typeFn = (val, row) => {
    return typeof val === "function" ? val(row) : val;
  };
const formatters = (val, format) => {
  return typeof format === "function" ? format(val) : val;
};
  const formatters = (val, format) => {
    return typeof format === "function" ? format(val) : val;
  };
// Props(使用 defineProps çš„非 TS å½¢å¼ï¼‰
const props = defineProps({
  tableLoading: {
    type: Boolean,
    default: false,
  },
  height: {
    type: [Number, String],
    default: "calc(100vh - 22em)",
  },
  expandRowKeys: {
    type: Array,
    default: () => [],
  },
  summaryMethod: {
    type: Function,
    default: () => {},
  },
  rowClick: {
    type: Function,
    default: () => {},
  },
  currentChange: {
    type: Function,
    default: () => {},
  },
  border: {
    type: Boolean,
    default: true,
  },
  isSelection: {
    type: Boolean,
    default: false,
  },
    isShowPagination: {
    type: Boolean,
    default: true,
  },
  isShowSummary: {
    type: Boolean,
    default: false,
  },
  highlightCurrentRow: {
    type: Boolean,
    default: false,
  },
  headerCellStyle: {
    type: Object,
    default: () => ({}),
  },
  column: {
    type: Array,
    default: () => [],
  },
  rowClassName: {
    type: Function,
    default: () => "",
  },
  rowStyle: {
    type: [Object, Function],
    default: () => ({}),
  },
  tableData: {
    type: Array,
    default: () => [],
  },
  rowKey: {
    type: String,
    default: 'id',
  },
  page: {
    type: Object,
    default: () => ({
      total: 0,
      current: 0,
      size: 10,
      layout: "total, sizes, prev, pager, next, jumper",
    }),
  },
  total: {
    type: Number,
    default: 0,
  },
  tableStyle: {
    type: [String, Object],
    default: () => ({ width: "100%" }),
  },
});
  // Props(使用 defineProps çš„非 TS å½¢å¼ï¼‰
  const props = defineProps({
    tableLoading: {
      type: Boolean,
      default: false,
    },
    height: {
      type: [Number, String],
      default: "calc(100vh - 22em)",
    },
    expandRowKeys: {
      type: Array,
      default: () => [],
    },
    summaryMethod: {
      type: Function,
      default: () => {},
    },
    rowClick: {
      type: Function,
      default: () => {},
    },
    currentChange: {
      type: Function,
      default: () => {},
    },
    border: {
      type: Boolean,
      default: true,
    },
    isSelection: {
      type: Boolean,
      default: false,
    },
    selectable: {
      type: Function,
      default: () => true,
    },
    isShowPagination: {
      type: Boolean,
      default: true,
    },
    isShowSummary: {
      type: Boolean,
      default: false,
    },
    highlightCurrentRow: {
      type: Boolean,
      default: false,
    },
    headerCellStyle: {
      type: Object,
      default: () => ({}),
    },
    column: {
      type: Array,
      default: () => [],
    },
    rowClassName: {
      type: Function,
      default: () => "",
    },
    rowStyle: {
      type: [Object, Function],
      default: () => ({}),
    },
    tableData: {
      type: Array,
      default: () => [],
    },
    rowKey: {
      type: String,
      default: "id",
    },
    page: {
      type: Object,
      default: () => ({
        total: 0,
        current: 0,
        size: 10,
        layout: "total, sizes, prev, pager, next, jumper",
      }),
    },
    total: {
      type: Number,
      default: 0,
    },
    tableStyle: {
      type: [String, Object],
      default: () => ({ width: "100%" }),
    },
  });
const mergedHeaderCellStyle = computed(() => ({
  background: "var(--surface-soft)",
  color: "var(--text-secondary)",
  fontWeight: 600,
  ...props.headerCellStyle,
}));
  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({});
  // Data
  const uploadRefs = ref([]);
  const currentFiles = ref({});
  const uploadKeys = ref({});
const indexMethod = (index) => {
  return (props.page.current - 1) * props.page.size + index + 1;
};
  const indexMethod = index => {
    return (props.page.current - 1) * props.page.size + index + 1;
  };
// ç‚¹å‡» link äº‹ä»¶
const goLink = (row, linkMethod) => {
  if (!linkMethod) {
    return ElMessage.warning("请配置 link äº‹ä»¶");
  }
  const parentMethod = getParentMethod(linkMethod);
  if (typeof parentMethod === "function") {
    parentMethod(row);
  } else {
    console.warn(`父组件中未找到方法: ${linkMethod}`);
  }
};
// èŽ·å–çˆ¶ç»„ä»¶æ–¹æ³•ï¼ˆç¤ºä¾‹å®žçŽ°ï¼‰
const getParentMethod = (methodName) => {
  const parentMethods = inject("parentMethods", {});
  return parentMethods[methodName];
};
const dataTypeFn = (val, format) => {
  if (typeof format === "function") {
    return format(val);
  } else return val;
};
const validTagTypes = ["primary", "success", "info", "warning", "danger"];
const formatType = (val, format) => {
  const type = typeof format === "function" ? format(val) : undefined;
  return validTagTypes.includes(type) ? type : undefined;
};
const isOperationDisabled = (operation, row) => {
  if (!operation?.disabled) return false;
  return typeof operation.disabled === "function"
    ? !!operation.disabled(row)
    : !!operation.disabled;
};
const parseHexToRgb = (hex) => {
  const normalized = String(hex || "").trim().replace("#", "");
  if (normalized.length === 3) {
    const r = parseInt(normalized[0] + normalized[0], 16);
    const g = parseInt(normalized[1] + normalized[1], 16);
    const b = parseInt(normalized[2] + normalized[2], 16);
    if ([r, g, b].some((n) => Number.isNaN(n))) return null;
    return { r, g, b };
  }
  if (normalized.length === 6 || normalized.length === 8) {
    const r = parseInt(normalized.slice(0, 2), 16);
    const g = parseInt(normalized.slice(2, 4), 16);
    const b = parseInt(normalized.slice(4, 6), 16);
    if ([r, g, b].some((n) => Number.isNaN(n))) return null;
    return { r, g, b };
  }
  return null;
};
const fadeColor = (color, alpha = 0.35) => {
  const c = String(color || "").trim();
  if (!c) return undefined;
  if (c.startsWith("#")) {
    const rgb = parseHexToRgb(c);
    if (!rgb) return c;
    return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
  }
  const rgbMatch = c.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*[\d.]+\s*)?\)$/i);
  if (rgbMatch) {
    const r = Number(rgbMatch[1]);
    const g = Number(rgbMatch[2]);
    const b = Number(rgbMatch[3]);
    if ([r, g, b].some((n) => Number.isNaN(n))) return c;
    return `rgba(${r}, ${g}, ${b}, ${alpha})`;
  }
  if (c.includes("--el-color-primary")) {
    return "var(--el-color-primary-light-5)";
  }
  if (c.includes("--el-color-danger")) {
    return "var(--el-color-danger-light-5)";
  }
  return "var(--el-text-color-disabled)";
};
const getOperationColor = (operation, row) => {
  const baseColor =
    operation?.name === "删除" || operation?.name === "delete"
      ? "#D93025"
      : operation?.name === "详情"
      ? "#67C23A"
      : operation?.color || "var(--el-color-primary)";
  if (isOperationDisabled(operation, row)) {
    return fadeColor(baseColor, 0.35);
  }
  return baseColor;
};
// æ–‡ä»¶å˜åŒ–处理
const handleChange = (file, fileList, index) => {
  if (fileList.length > 1) {
    const earliestFile = fileList[0];
    uploadRefs.value[index]?.handleRemove(earliestFile);
  }
  currentFiles.value[index] = file;
};
// æ–‡ä»¶ä¸Šä¼ å‰æ ¡éªŒ
const beforeUpload = (rawFile, index) => {
  currentFiles.value[index] = {};
  if (rawfile.size > 1024 * 1024 * 10 * 10) {
    ElMessage.error("上传文件不超过10M");
    return false;
  }
  return true;
};
// ä¸Šä¼ æˆåŠŸ
const handleSuccessUp = (response, file, fileList, index) => {
  if (response.code == 200) {
    if (uploadRefs[index]) {
      uploadRefs[index].clearFiles();
  // ç‚¹å‡» link äº‹ä»¶
  const goLink = (row, linkMethod) => {
    if (!linkMethod) {
      return ElMessage.warning("请配置 link äº‹ä»¶");
    }
    currentFiles[index] = file;
    ElMessage.success("上传成功");
    resetUploadComponent(index);
  } else {
    ElMessage.error(response.message);
  }
};
    const parentMethod = getParentMethod(linkMethod);
    if (typeof parentMethod === "function") {
      parentMethod(row);
    } else {
      console.warn(`父组件中未找到方法: ${linkMethod}`);
    }
  };
const resetUploadComponent = (index) => {
  uploadKeys[index] = Date.now();
};
  // èŽ·å–çˆ¶ç»„ä»¶æ–¹æ³•ï¼ˆç¤ºä¾‹å®žçŽ°ï¼‰
  const getParentMethod = methodName => {
    const parentMethods = inject("parentMethods", {});
    return parentMethods[methodName];
  };
// ä¸Šä¼ å¤±è´¥
const onError = (error, file, fileList, index) => {
  ElMessage.error("文件上传失败,请重试");
  if (uploadRefs.value[index]) {
    uploadRefs.value[index].clearFiles();
  }
};
  const dataTypeFn = (val, format) => {
    if (typeof format === "function") {
      return format(val);
    } else return val;
  };
  const validTagTypes = ["primary", "success", "info", "warning", "danger"];
// æ–‡ä»¶æ•°é‡è¶…限提示
const onExceed = () => {
  ElMessage.warning("超出文件个数");
};
  const formatType = (val, format) => {
    const type = typeof format === "function" ? format(val) : undefined;
    return validTagTypes.includes(type) ? type : undefined;
  };
const paginationSearch = ({ page, limit }) => {
  emit("pagination", { page: page, limit: limit });
};
  const isOperationDisabled = (operation, row) => {
    if (!operation?.disabled) return false;
    return typeof operation.disabled === "function"
      ? !!operation.disabled(row)
      : !!operation.disabled;
  };
const rowClick = (row) => {
  emit("row-click", row);
};
  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 expandChange = (row, expandedRows) => {
  emit("expand-change", row, expandedRows);
};
  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 handleSelectionChange = (newSelection) => {
  emit("selection-change", newSelection);
};
  const getOperationColor = (operation, row) => {
    const baseColor =
      operation?.name === "删除" || operation?.name === "delete"
        ? "#D93025"
        : operation?.name === "详情"
        ? "#67C23A"
        : operation?.color || "var(--el-color-primary)";
    if (isOperationDisabled(operation, row)) {
      return fadeColor(baseColor, 0.35);
    }
    return baseColor;
  };
  // æ–‡ä»¶å˜åŒ–处理
  const handleChange = (file, fileList, index) => {
    if (fileList.length > 1) {
      const earliestFile = fileList[0];
      uploadRefs.value[index]?.handleRemove(earliestFile);
    }
    currentFiles.value[index] = file;
  };
  // æ–‡ä»¶ä¸Šä¼ å‰æ ¡éªŒ
  const beforeUpload = (rawFile, index) => {
    currentFiles.value[index] = {};
    if (rawfile.size > 1024 * 1024 * 10 * 10) {
      ElMessage.error("上传文件不超过10M");
      return false;
    }
    return true;
  };
  // ä¸Šä¼ æˆåŠŸ
  const handleSuccessUp = (response, file, fileList, index) => {
    if (response.code == 200) {
      if (uploadRefs[index]) {
        uploadRefs[index].clearFiles();
      }
      currentFiles[index] = file;
      ElMessage.success("上传成功");
      resetUploadComponent(index);
    } else {
      ElMessage.error(response.message);
    }
  };
  const resetUploadComponent = index => {
    uploadKeys[index] = Date.now();
  };
  // ä¸Šä¼ å¤±è´¥
  const onError = (error, file, fileList, index) => {
    ElMessage.error("文件上传失败,请重试");
    if (uploadRefs.value[index]) {
      uploadRefs.value[index].clearFiles();
    }
  };
  // æ–‡ä»¶æ•°é‡è¶…限提示
  const onExceed = () => {
    ElMessage.warning("超出文件个数");
  };
  const paginationSearch = ({ page, limit }) => {
    emit("pagination", { page: page, limit: limit });
  };
  const rowClick = row => {
    emit("row-click", row);
  };
  const expandChange = (row, expandedRows) => {
    emit("expand-change", row, expandedRows);
  };
  const handleSelectionChange = newSelection => {
    emit("selection-change", newSelection);
  };
</script>
<style scoped lang="scss">
.lims-table {
  border: 1px solid var(--surface-border);
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.9);
}
  .lims-table {
    border: 1px solid var(--surface-border);
    border-radius: 18px;
    background: rgba(255, 255, 255, 0.9);
  }
.cell {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding-right: 0 !important;
  padding-left: 0 !important;
}
  .cell {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    padding-right: 0 !important;
    padding-left: 0 !important;
  }
.pim-table-header-extra :deep(.el-input),
.pim-table-header-extra :deep(.el-select) {
  width: 100%;
}
  .pim-table-header-extra :deep(.el-input),
  .pim-table-header-extra :deep(.el-select) {
    width: 100%;
  }
.pim-table-header-title {
  font-weight: 600;
}
  .pim-table-header-title {
    font-weight: 600;
  }
</style>
src/components/ProcessParamListDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,670 @@
<template>
  <el-dialog v-model="visible"
             :title="title"
             width="800px"
             destroy-on-close>
    <div class="param-list-container">
      <div class="params-header">
        <span>参数列表</span>
        <div>
          <el-button v-if="editable"
                     type="primary"
                     link
                     size="small"
                     @click="handleAddParam">
            <el-icon>
              <Plus />
            </el-icon>新增
          </el-button>
          <!-- <el-button v-if="editable"
                     type="primary"
                     link
                     size="small"
                     @click="getsyncProcessParamItem">
            <el-icon>
              <Refresh />
            </el-icon>同步工序参数
          </el-button> -->
        </div>
      </div>
      <div class="params-list">
        <div v-for="param in paramList"
             :key="param.id"
             class="param-item">
          <div class="param-info">
            <span class="param-code">{{ param.paramName }}</span>
            <span class="param-value">
              æ ‡å‡†å€¼ï¼š{{ param.standardValue || "-" }} {{ param.unit }}
            </span>
          </div>
          <div class="param-actions">
            <el-button v-if="editable"
                       link
                       type="primary"
                       size="small"
                       @click="handleEditParam(param)">
              ç¼–辑
            </el-button>
            <el-button v-if="editable"
                       link
                       type="danger"
                       size="small"
                       @click="handleDeleteParam(param)">
              åˆ é™¤
            </el-button>
          </div>
        </div>
        <el-empty v-if="!paramList || paramList.length === 0"
                  description="暂无参数"
                  :image-size="50" />
      </div>
    </div>
    <!-- é€‰æ‹©å‚数对话框 -->
    <el-dialog v-model="selectParamDialogVisible"
               title="选择参数"
               width="1000px">
      <div class="param-select-container">
        <!-- å·¦ä¾§å‚数列表 -->
        <div class="param-list-area">
          <div class="area-title">可选参数</div>
          <div class="search-box">
            <el-input v-model="paramSearchKeyword"
                      placeholder="请输入参数名称搜索"
                      clearable
                      size="small"
                      @input="getBaseParamListData">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredParamList"
                    height="400"
                    border
                    highlight-current-row
                    @current-change="handleSelectParam">
            <el-table-column prop="paramName"
                             label="参数名称" />
            <el-table-column prop="paramType"
                             label="参数类型">
              <template #default="scope">
                <el-tag size="small"
                        :type="getParamTypeTag(scope.row.paramType)">{{ getParamTypeText(scope.row.paramType) }}</el-tag>
              </template>
            </el-table-column>
          </el-table>
          <!-- åˆ†é¡µæŽ§ä»¶ -->
          <div class="pagination-container"
               style="margin-top: 10px;">
            <el-pagination :current-page="paramPage.current"
                           :page-size="paramPage.size"
                           :page-sizes="[10, 20, 50, 100]"
                           layout="total, sizes, prev, pager, next, jumper"
                           :total="paramPage.total"
                           @size-change="getBaseParamListData"
                           @current-change="getBaseParamListData"
                           size="small" />
          </div>
        </div>
        <!-- å³ä¾§å‚数详情 -->
        <div class="param-detail-area">
          <div class="area-title">参数详情</div>
          <el-form v-if="selectedParam"
                   :model="selectedParam"
                   label-width="100px"
                   class="param-detail-form">
            <el-form-item label="参数名称">
              <span class="detail-text">{{ selectedParam.paramName }}</span>
            </el-form-item>
            <el-form-item label="参数类型">
              <el-tag size="small"
                      :type="getParamTypeTag(selectedParam.paramType)">{{ getParamTypeText(selectedParam.paramType) }}</el-tag>
            </el-form-item>
            <el-form-item label="参数格式">
              <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
            </el-form-item>
            <el-form-item label="单位">
              <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
            </el-form-item>
            <el-form-item label="标准值">
              <el-input v-model="selectedParam.standardValue"
                        @input="val => onStandardValueInput(val, selectedParam)"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch :active-value="1"
                         :inactive-value="0"
                         v-model="selectedParam.isRequired" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择参数"
                    :image-size="100" />
        </div>
      </div>
      <template #footer>
        <el-button type="primary"
                   @click="handleParamSelectSubmit">确定</el-button>
        <el-button @click="selectParamDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
    <!-- ç¼–辑参数对话框 -->
    <el-dialog v-model="editParamDialogVisible"
               title="编辑参数"
               width="600px">
      <el-form :model="editParamForm"
               :rules="editParamRules"
               ref="editParamFormRef"
               label-width="120px">
        <el-form-item label="参数名称">
          <span class="detail-text">{{ editParamForm.paramName }}</span>
        </el-form-item>
        <el-form-item label="参数类型">
          <el-tag size="small"
                  :type="getParamTypeTag(editParamForm.paramType)">
            {{ getParamTypeText(editParamForm.paramType) }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数格式">
          <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span>
        </el-form-item>
        <el-form-item label="单位">
          <span class="detail-text">{{ editParamForm.unit || '-' }}</span>
        </el-form-item>
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    @input="val => onStandardValueInput(val, editParamForm)"
                    placeholder="请输入标准值" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary"
                   @click="handleEditParamSubmit">确定</el-button>
        <el-button @click="editParamDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
  </el-dialog>
</template>
<script setup>
  import { ref, computed, watch } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { Plus, Search } from "@element-plus/icons-vue";
  import {
    delProcessRouteItemParam,
    editProcessRouteItemParam,
    addProcessRouteItemParam,
  } from "@/api/productionManagement/processRouteItem.js";
  import {
    addProcessRouteItemParamOrder,
    delProcessRouteItemParamOrder,
    editProcessRouteItemParamOrder,
  } from "@/api/productionManagement/productProcessRoute.js";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  const props = defineProps({
    modelValue: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: "参数列表",
    },
    routeId: {
      type: Number,
      default: 0,
    },
    process: {
      type: Object,
      default: () => ({}),
    },
    paramList: {
      type: Array,
      default: () => [],
    },
    editable: {
      type: Boolean,
      default: true,
    },
    orderId: {
      type: Number,
      default: 0,
    },
    pageType: {
      type: String,
      default: "route",
    },
  });
  const emit = defineEmits(["update:modelValue", "refresh"]);
  const visible = computed({
    get: () => props.modelValue,
    set: value => emit("update:modelValue", value),
  });
  // å“åº”式数据
  const selectParamDialogVisible = ref(false);
  const editParamDialogVisible = ref(false);
  const paramSearchKeyword = ref("");
  const selectedParam = ref(null);
  const filteredParamList = ref([]);
  const paramPage = ref({
    current: 1,
    size: 10,
    total: 0,
  });
  const editParamForm = ref({
    id: null,
    processId: null,
    paramId: null,
    paramName: "",
    standardValue: null,
    isRequired: 0,
    paramType: null,
    paramFormat: "",
    unit: "",
  });
  const onStandardValueInput = (val, target) => {
    const data = target.value || target;
    const type = data.paramType || data.parameterType;
    if (type === 1) {
      // æ•°å€¼æ ¼å¼ï¼šä¸èƒ½è¾“入中文或英文字符
      data.standardValue = val.replace(/[a-zA-Z\u4e00-\u9fa5]/g, "");
    }
  };
  const editParamRules = ref({
    standardValue: [
      {
        validator: (rule, value, callback) => {
          const type =
            editParamForm.value.paramType || editParamForm.value.parameterType;
          if (type === 1 && value) {
            if (/[a-zA-Z\u4e00-\u9fa5]/.test(value)) {
              return callback(new Error("数值格式不能包含中英文字符"));
            }
          }
          callback();
        },
        trigger: "blur",
      },
    ],
  });
  const editParamFormRef = ref(null);
  // æ–°å¢žå‚æ•°
  const handleAddParam = () => {
    selectedParam.value = null;
    paramSearchKeyword.value = "";
    paramPage.value.current = 1;
    // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
    getBaseParamListData();
    selectParamDialogVisible.value = true;
  };
  // ç¼–辑参数
  const handleEditParam = param => {
    editParamForm.value = {
      id: param.id,
      processId: props.process.id,
      paramId: param.paramId,
      paramName: param.parameterName || param.paramName,
      standardValue: param.standardValue,
      isRequired: param.isRequired || 0,
      paramType: param.parameterType || param.paramType,
      paramFormat: param.parameterFormat || param.paramFormat,
      unit: param.unit || param.unit,
    };
    editParamDialogVisible.value = true;
  };
  // åˆ é™¤å‚æ•°
  const handleDeleteParam = param => {
    ElMessageBox.confirm("确定要删除该参数吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // è°ƒç”¨API删除参数
        if (props.pageType === "order") {
          delProcessRouteItemParamOrder(param.id)
            .then(res => {
              ElMessage.success("删除成功");
              emit("refresh");
            })
            .catch(err => {
              ElMessage.error("删除参数失败");
              console.error("删除参数失败:", err);
            });
        } else {
          delProcessRouteItemParam(param.id)
            .then(res => {
              ElMessage.success("删除成功");
              emit("refresh");
            })
            .catch(err => {
              ElMessage.error("删除参数失败");
              console.error("删除参数失败:", err);
            });
        }
      })
      .catch(() => {});
  };
  const getsyncProcessParamItem = () => {
    emit("getsyncProcessParamItem");
  };
  // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
  const getBaseParamListData = () => {
    console.log(paramPage, "paramPage.size");
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.value.current,
      size: paramPage.value.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.value.total = res.data.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // é€‰æ‹©å‚æ•°
  const handleSelectParam = param => {
    selectedParam.value = param;
  };
  // æäº¤é€‰æ‹©å‚æ•°
  const handleParamSelectSubmit = () => {
    if (!selectedParam.value) {
      ElMessage.warning("请先选择一个参数");
      return;
    }
    if (!props.process || !props.process.id) {
      ElMessage.error("工艺路线项目信息不完整");
      return;
    }
    // è°ƒç”¨API新增参数
    if (props.pageType === "order") {
      addProcessRouteItemParamOrder({
        productionOrderId: Number(props.orderId),
        productionOrderRoutingOperationId: props.process.id,
        technologyRoutingOperationParamId: props.process.id,
        paramId: selectedParam.value.id,
        standardValue: selectedParam.value.standardValue || "",
        isRequired: selectedParam.value.isRequired || 0,
      })
        .then(res => {
          if (res.code === 200) {
            ElMessage.success("添加参数成功");
            selectParamDialogVisible.value = false;
            emit("refresh");
          } else {
            ElMessage.error(res.msg || "添加参数失败");
          }
        })
        .catch(err => {
          ElMessage.error("添加参数失败");
          console.error("添加参数失败:", err);
        });
    } else {
      console.log(selectedParam.value, "selectedParam");
      addProcessRouteItemParam({
        technologyRoutingOperationId: props.process.id,
        paramId: selectedParam.value.id,
        standardValue: selectedParam.value.standardValue || "",
        isRequired: selectedParam.value.isRequired || 0,
      })
        .then(res => {
          if (res.code === 200) {
            ElMessage.success("添加参数成功");
            selectParamDialogVisible.value = false;
            emit("refresh");
          } else {
            ElMessage.error(res.msg || "添加参数失败");
          }
        })
        .catch(err => {
          ElMessage.error("添加参数失败");
          console.error("添加参数失败:", err);
        });
    }
  };
  // æäº¤ç¼–辑参数
  const handleEditParamSubmit = () => {
    if (!editParamFormRef.value) return;
    editParamFormRef.value.validate(valid => {
      if (valid) {
        if (props.pageType === "order") {
          editProcessRouteItemParamOrder({
            id: editParamForm.value.id,
            standardValue: editParamForm.value.standardValue || "",
            isRequired: editParamForm.value.isRequired || 0,
            // productionOrderRoutingOperationId: props.process.id,
          })
            .then(res => {
              if (res.code === 200) {
                ElMessage.success("编辑成功");
                editParamDialogVisible.value = false;
                emit("refresh");
              } else {
                ElMessage.error(res.msg || "编辑失败");
              }
            })
            .catch(err => {
              ElMessage.error("编辑参数失败");
              console.error("编辑参数失败:", err);
            });
        } else {
          // è°ƒç”¨API修改参数
          editProcessRouteItemParam({
            id: editParamForm.value.id,
            technologyRoutingOperationId: props.process.id,
            paramId: editParamForm.value.paramId,
            standardValue: editParamForm.value.standardValue || "",
            isRequired: editParamForm.value.isRequired || 0,
          })
            .then(res => {
              if (res.code === 200) {
                ElMessage.success("编辑成功");
                editParamDialogVisible.value = false;
                emit("refresh");
              } else {
                ElMessage.error(res.msg || "编辑失败");
              }
            })
            .catch(err => {
              ElMessage.error("编辑参数失败");
              console.error("编辑参数失败:", err);
            });
        }
      }
    });
  };
  // èŽ·å–å‚æ•°ç±»åž‹æ ‡ç­¾
  const getParamTypeTag = type => {
    const typeMap = {
      1: "primary",
      2: "info",
      3: "warning",
      4: "success",
    };
    return typeMap[type] || "default";
  };
  // èŽ·å–å‚æ•°ç±»åž‹æ–‡æœ¬
  const getParamTypeText = type => {
    const typeMap = {
      1: "数值格式",
      2: "文本格式",
      3: "下拉选项",
      4: "时间格式",
    };
    return typeMap[type] || type;
  };
  watch(
    () => props.modelValue,
    newVal => {
      if (!newVal) {
        // å¼¹çª—关闭时重置数据
        selectParamDialogVisible.value = false;
        editParamDialogVisible.value = false;
        selectedParam.value = null;
        paramSearchKeyword.value = "";
        paramPage.value.current = 1;
        filteredParamList.value = [];
        editParamForm.value = {
          id: null,
          processId: null,
          paramId: null,
          paramName: "",
          standardValue: null,
          isRequired: 0,
          paramType: null,
          paramFormat: "",
          unit: "",
        };
      }
    }
  );
</script>
<style scoped>
  .param-list-container {
    padding: 10px 0;
  }
  .params-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    padding-bottom: 10px;
    border-bottom: 1px solid #e4e7ed;
  }
  .params-header span {
    font-size: 16px;
    font-weight: 500;
    color: #303133;
  }
  .params-list {
    max-height: 400px;
    overflow-y: auto;
  }
  .param-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 16px;
    margin-bottom: 8px;
    background-color: #f9f9f9;
    border-radius: 4px;
    transition: all 0.3s ease;
  }
  .param-item:hover {
    background-color: #ecf5ff;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  .param-info {
    display: flex;
    align-items: center;
    gap: 20px;
    flex: 1;
  }
  .param-code {
    font-weight: 500;
    color: #303133;
    min-width: 120px;
  }
  .param-value {
    color: #606266;
    font-size: 14px;
  }
  .param-actions {
    display: flex;
    gap: 10px;
  }
  /* æ»šåŠ¨æ¡æ ·å¼ */
  .params-list::-webkit-scrollbar {
    width: 6px;
  }
  .params-list::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 3px;
  }
  .params-list::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
  }
  .params-list::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  /* é€‰æ‹©å‚数对话框样式 */
  .param-select-container {
    display: flex;
    gap: 20px;
  }
  .param-list-area {
    flex: 1;
    min-width: 400px;
  }
  .param-detail-area {
    flex: 1;
    min-width: 300px;
  }
  .area-title {
    font-size: 14px;
    font-weight: 500;
    margin-bottom: 10px;
    color: #303133;
  }
  .search-box {
    display: flex;
    gap: 10px;
    margin-bottom: 10px;
  }
  .param-detail-form {
    background: #f9f9f9;
    padding: 15px;
    border-radius: 4px;
  }
  .detail-text {
    font-weight: 500;
  }
</style>
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,10 @@
<template>
  <AIChatSidebar :assistants="assistants" default-assistant="purchase" />
</template>
<script setup>
import AIChatSidebar from '@/components/AIChatSidebar/index.vue'
import { purchaseAssistant } from '@/components/AIChatSidebar/assistants'
const assistants = [purchaseAssistant]
</script>
src/components/SvgIcon/index.vue
@@ -9,7 +9,7 @@
  props: {
    iconClass: {
      type: String,
      required: true
      default: ''
    },
    className: {
      type: String,
src/components/filePreview/index.vue
@@ -78,9 +78,9 @@
  transformData: (workbookData) => workbookData,
});
// è®¡ç®—属性 - åˆ¤æ–­æ–‡ä»¶ç±»åž‹
// è®¡ç®—属性 - åˆ¤æ–­æ–‡ä»¶ç±»åž‹ï¼ˆæ”¯æŒURL带查询参数)
const isImage = computed(() => {
  const state = /\.(jpg|jpeg|png|gif)$/i.test(fileUrl.value);
  const state = /\.(jpg|jpeg|png|gif)(\?.*)?$/i.test(fileUrl.value);
  if (state) {
    imgUrl.value = fileUrl.value.replaceAll('word', 'img');
  }
@@ -89,23 +89,23 @@
const isPdf = computed(() => {
  console.log(fileUrl.value)
  return /\.pdf$/i.test(fileUrl.value);
  return /\.pdf(\?.*)?$/i.test(fileUrl.value);
});
const isDoc = computed(() => {
  return /\.(doc|docx)$/i.test(fileUrl.value);
  return /\.(doc|docx)(\?.*)?$/i.test(fileUrl.value);
});
const isXls = computed(() => {
  const state = /\.(xls|xlsx)$/i.test(fileUrl.value);
  const state = /\.(xls|xlsx)(\?.*)?$/i.test(fileUrl.value);
  if (state) {
    options.value.xls = /\.(xls)$/i.test(fileUrl.value);
    options.value.xls = /\.(xls)(\?.*)?$/i.test(fileUrl.value);
  }
  return state;
});
const isZipOrRar = computed(() => {
  return /\.(zip|rar)$/i.test(fileUrl.value);
  return /\.(zip|rar)(\?.*)?$/i.test(fileUrl.value);
});
const isSupported = computed(() => {
@@ -164,7 +164,7 @@
};
const open = (url) => {
  fileUrl.value = window.location.protocol+'//'+window.location.host+ url;
  fileUrl.value = url;
  dialogVisible.value = true;
};
const handleClose = () => {
src/layout/components/Sidebar/index.vue
@@ -1,142 +1,142 @@
<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"
          :base-path="route.path"
        />
      <el-menu :default-active="activeMenu"
               :collapse="isCollapse"
               :background-color="getMenuBackground"
               :text-color="getMenuTextColor"
               :unique-opened="true"
               :active-text-color="theme"
               :collapse-transition="false"
               mode="vertical"
               :class="sideTheme">
        <sidebar-item v-for="(route, index) in sidebarRouters"
                      :key="route.path + index"
                      :item="route"
                      :base-path="route.path" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>
<script setup>
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import 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(() => 'var(--sidebar-bg)')
  const getMenuBackground = computed(() => "var(--sidebar-bg)");
const getMenuTextColor = computed(() => {
  if (settingsStore.isDark) {
    return 'var(--sidebar-text)'
  }
  return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText
})
  const getMenuTextColor = computed(() => {
    if (settingsStore.isDark) {
      return "var(--sidebar-text)";
    }
    return sideTheme.value === "theme-dark"
      ? variables.menuText
      : variables.menuLightText;
  });
const activeMenu = computed(() => {
  const { meta, path } = route
  if (meta.activeMenu) {
    return meta.activeMenu
  }
  return path
})
  const activeMenu = computed(() => {
    const { meta, path } = route;
    if (meta.activeMenu) {
      return meta.activeMenu;
    }
    return path;
  });
</script>
<style lang="scss" scoped>
.sidebar-container {
  background-color: v-bind(getMenuBackground);
  border-radius: 22px;
  overflow: hidden;
  .scrollbar-wrapper {
  .sidebar-container {
    background-color: v-bind(getMenuBackground);
  }
  .el-menu {
    border: none;
    height: 100%;
    width: 100% !important;
    border-radius: 22px;
    overflow: hidden;
    .el-menu-item,
    .el-sub-menu__title {
      margin-bottom: 6px;
      border-radius: 14px;
      color: v-bind(getMenuTextColor);
    .scrollbar-wrapper {
      background-color: v-bind(getMenuBackground);
    }
      &:hover {
        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
    .el-menu {
      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 {
        &.is-active {
          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;
      }
    }
    .el-menu-item {
      &.is-active {
        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/index.vue
@@ -1,131 +1,142 @@
<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="{ 'fixed-header': fixedHeader }">
        <navbar @setLayout="setLayout" />
        <tags-view v-if="needTagsView" />
      </div>
      <app-main />
      <settings ref="settingRef" />
    </div>
  </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 useAppStore from "@/store/modules/app";
  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 classObj = computed(() => ({
    hideSidebar: !sidebar.value.opened,
    openSidebar: sidebar.value.opened,
    withoutAnimation: sidebar.value.withoutAnimation,
    mobile: device.value === "mobile",
  }));
  const { width, height } = useWindowSize();
  const WIDTH = 992; // refer to Bootstrap's responsive design
  watch(
    () => device.value,
    () => {
      if (device.value === "mobile" && sidebar.value.opened) {
        useAppStore().closeSideBar({ withoutAnimation: false });
      }
    }
  );
  watchEffect(() => {
    if (width.value - 1 < WIDTH) {
      useAppStore().toggleDevice("mobile");
      useAppStore().closeSideBar({ withoutAnimation: true });
    } else {
      useAppStore().toggleDevice("desktop");
    }
  });
  function handleClickOutside() {
    useAppStore().closeSideBar({ withoutAnimation: false });
  }
  const settingRef = ref(null);
  function setLayout() {
    settingRef.value.openSetting();
  }
</script>
<style lang="scss" scoped>
  @import "@/assets/styles/mixin.scss";
  @import "@/assets/styles/variables.module.scss";
  .app-wrapper {
    @include clearfix;
    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;
      top: 0;
    }
  }
  .drawer-bg {
    background: #000;
    opacity: 0.3;
    width: 100%;
    top: 0;
    height: 100%;
    position: absolute;
    z-index: 999;
  }
  .fixed-header {
    position: fixed;
    top: 0px;
    padding-top: 12px;
    right: 16px;
    z-index: 9;
    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% - 100px);
  }
  .sidebarHide .fixed-header {
    width: calc(100% - 32px);
  }
  .mobile .fixed-header {
    width: 100%;
  }
</style>
<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="{ 'fixed-header': fixedHeader }">
        <navbar @setLayout="setLayout" />
        <tags-view v-if="needTagsView" />
      </div>
      <app-main />
      <settings ref="settingRef" />
    </div>
    <AIChatSidebar v-if="showGlobalAiChat" />
  </div>
</template>
<script setup>
  import { useWindowSize } from "@vueuse/core";
  import { useRoute } from "vue-router";
  import Sidebar from "./components/Sidebar/index.vue";
  import { AppMain, Navbar, Settings, TagsView } from "./components";
  import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
  import defaultSettings from "@/settings";
  import useAppStore from "@/store/modules/app";
  import useUserStore from "@/store/modules/user";
  import useSettingsStore from "@/store/modules/settings";
  const settingsStore = useSettingsStore();
  const userStore = useUserStore();
  const route = useRoute();
  const theme = computed(() => settingsStore.theme);
  const sideTheme = computed(() => settingsStore.sideTheme);
  const sidebar = computed(() => useAppStore().sidebar);
  const device = computed(() => useAppStore().device);
  const needTagsView = computed(() => settingsStore.tagsView);
  const fixedHeader = computed(() => settingsStore.fixedHeader);
  const aiEnabled = computed(() => Number(userStore.aiEnabled) === 1);
  const showGlobalAiChat = computed(() => {
    const isIndustrialBrainRoute = String(route.path || "").startsWith("/ai-industrial-brain");
    return !isIndustrialBrainRoute && aiEnabled.value;
  });
  const classObj = computed(() => ({
    hideSidebar: !sidebar.value.opened,
    openSidebar: sidebar.value.opened,
    withoutAnimation: sidebar.value.withoutAnimation,
    mobile: device.value === "mobile",
  }));
  const { width, height } = useWindowSize();
  const WIDTH = 992; // refer to Bootstrap's responsive design
  watch(
    () => device.value,
    () => {
      if (device.value === "mobile" && sidebar.value.opened) {
        useAppStore().closeSideBar({ withoutAnimation: false });
      }
    }
  );
  watchEffect(() => {
    if (width.value - 1 < WIDTH) {
      useAppStore().toggleDevice("mobile");
      useAppStore().closeSideBar({ withoutAnimation: true });
    } else {
      useAppStore().toggleDevice("desktop");
    }
  });
  function handleClickOutside() {
    useAppStore().closeSideBar({ withoutAnimation: false });
  }
  const settingRef = ref(null);
  function setLayout() {
    settingRef.value.openSetting();
  }
</script>
<style lang="scss" scoped>
  @import "@/assets/styles/mixin.scss";
  @import "@/assets/styles/variables.module.scss";
  .app-wrapper {
    @include clearfix;
    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;
      top: 0;
    }
  }
  .drawer-bg {
    background: #000;
    opacity: 0.3;
    width: 100%;
    top: 0;
    height: 100%;
    position: absolute;
    z-index: 999;
  }
  .fixed-header {
    position: fixed;
    top: 0px;
    padding-top: 12px;
    right: 16px;
    z-index: 9;
    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% - 100px);
  }
  .sidebarHide .fixed-header {
    width: calc(100% - 32px);
  }
  .mobile .fixed-header {
    width: 100%;
  }
</style>
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/plugins/download.js
@@ -82,6 +82,16 @@
  saveAs(text, name, opts) {
    saveAs(text, name, opts);
  },
  byUrl(url, filename) {
    // å°†URL中的preview替换成download
    const downloadUrl = url.replace(/preview/g, 'download')
    const link = document.createElement('a')
    link.href = downloadUrl
    link.download = filename || ''
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  },
  async printErrMsg(data) {
    const resText = await data.text();
    const rspObj = JSON.parse(resText);
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"),
@@ -72,6 +86,18 @@
    ],
  },
  {
    path: "/ai-industrial-brain",
    component: Layout,
    children: [
      {
        path: "index",
        component: () => import("@/views/aiIndustrialBrain/index.vue"),
        name: "AiIndustrialBrain",
        meta: { title: "AI工业大脑", icon: "skill" },
      },
    ],
  },
  {
    path: "/user",
    component: Layout,
    hidden: true,
@@ -92,20 +118,6 @@
    name: "DeviceInfo",
    meta: { title: "设备信息", icon: "monitor" },
  },
  // æ·»åŠ é¡¹ç›®è¯¦æƒ…é¡µé¢è·¯ç”±é…ç½®
  {
    path: "/oaSystem/projectManagement/projectDetail",
    component: Layout,
    hidden: true,
    children: [
      {
        path: ":projectId",
        component: () => import("@/views/oaSystem/projectManagement/projectDetail.vue"),
        name: "ProjectDetail",
        meta: { title: "项目详情", activeMenu: "/oaSystem/projectManagement" },
      },
    ],
  },
  {
    path: "/projectManagement/Management/detail",
    component: Layout,
@@ -119,6 +131,127 @@
      },
    ],
  },
  // è´¢åŠ¡ç®¡ç†æ¨¡å—è·¯ç”±
  {
    path: "/financial",
    component: Layout,
    hidden: false,
    redirect: "/financial/general-ledger",
    alwaysShow: true,
    meta: { title: "财务管理", icon: "money" },
    children: [
      {
        path: "sales-out",
        component: () => import("@/views/financialManagement/receivable/salesOut.vue"),
        name: "SalesOut",
        meta: { title: "销售出库" },
      },
      {
        path: "sales-return",
        component: () => import("@/views/financialManagement/receivable/salesReturn.vue"),
        name: "SalesReturn",
        meta: { title: "销售退货" },
      },
      {
        path: "invoice-apply",
        component: () => import("@/views/financialManagement/receivable/invoiceApply.vue"),
        name: "InvoiceApply",
        meta: { title: "开票申请" },
      },
      {
        path: "output-invoice",
        component: () => import("@/views/financialManagement/receivable/outputInvoice.vue"),
        name: "OutputInvoice",
        meta: { title: "销项发票" },
      },
      {
        path: "receipt",
        component: () => import("@/views/financialManagement/receivable/receipt.vue"),
        name: "Receipt",
        meta: { title: "收款单" },
      },
      {
        path: "receivable-reconciliation",
        component: () => import("@/views/financialManagement/receivable/reconciliation.vue"),
        name: "ReceivableReconciliation",
        meta: { title: "应收对账" },
      },
      {
        path: "purchase-in",
        component: () => import("@/views/financialManagement/payable/purchaseIn.vue"),
        name: "PurchaseIn",
        meta: { title: "采购入库" },
      },
      {
        path: "purchase-return",
        component: () => import("@/views/financialManagement/payable/purchaseReturn.vue"),
        name: "PurchaseReturn",
        meta: { title: "采购退货" },
      },
      {
        path: "input-invoice",
        component: () => import("@/views/financialManagement/payable/input-invoice.vue"),
        name: "InputInvoice",
        meta: { title: "进项发票" },
      },
      {
        path: "payment-apply",
        component: () => import("@/views/financialManagement/payable/paymentApply.vue"),
        name: "PaymentApply",
        meta: { title: "付款申请" },
      },
      {
        path: "payment",
        component: () => import("@/views/financialManagement/payable/payment.vue"),
        name: "Payment",
        meta: { title: "付款单" },
      },
      {
        path: "payable-reconciliation",
        component: () => import("@/views/financialManagement/payable/reconciliation.vue"),
        name: "PayableReconciliation",
        meta: { title: "应付对账" },
      },
      {
        path: "fixed-assets",
        component: () => import("@/views/financialManagement/assets/fixedAssets.vue"),
        name: "FixedAssets",
        meta: { title: "固定资产" },
      },
      {
        path: "intangible-assets",
        component: () => import("@/views/financialManagement/assets/intangibleAssets.vue"),
        name: "IntangibleAssets",
        meta: { title: "无形资产" },
      },
      {
        path: "general-ledger",
        component: () => import("@/views/financialManagement/generalLedger/index.vue"),
        name: "GeneralLedger",
        meta: { title: "总帐科目" },
      },
      {
        path: "voucher",
        component: () => import("@/views/financialManagement/voucher/index.vue"),
        name: "Voucher",
        meta: { title: "凭证" },
      },
      {
        path: "voucher-general-ledger",
        component: () => import("@/views/financialManagement/voucher/generalLedger.vue"),
        name: "VoucherGeneralLedger",
        meta: { title: "科目总帐" },
      },
      {
        path: "voucher-detail-ledger",
        component: () => import("@/views/financialManagement/voucher/detailLedger.vue"),
        name: "VoucherDetailLedger",
        meta: { title: "科目明细帐" },
      },
    ],
  },
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
src/store/modules/permission.js
@@ -1,9 +1,10 @@
import auth from '@/plugins/auth'
import router, { constantRoutes, dynamicRoutes } from '@/router'
import { getRouters } from '@/api/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink'
import 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,28 +37,62 @@
        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)
            resolve(rewriteRoutes)
          })
        })
            const defaultRoutes = filterAsyncRouter(defaultData)
            const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
            asyncRoutes.forEach(route => { router.addRoute(route) })
            this.setRoutes(rewriteRoutes)
            const constantSidebarRoutes = filterAiFeatureRoutes(constantRoutes, aiEnabled)
            // å°†è´¢åŠ¡ç®¡ç†è·¯ç”±åˆå¹¶åˆ°ä¾§è¾¹æ 
            this.setSidebarRouters(constantSidebarRoutes.concat(sidebarRoutes))
            this.setDefaultRoutes(sidebarRoutes)
            this.setTopbarRoutes(defaultRoutes)
            resolve(rewriteRoutes)
          })
        })
      }
    }
  })
// éåŽ†åŽå°ä¼ æ¥çš„è·¯ç”±å­—ç¬¦ä¸²ï¼Œè½¬æ¢ä¸ºç»„ä»¶å¯¹è±¡
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
function filterAiFeatureRoutes(routes = [], aiEnabled = false) {
  if (aiEnabled) {
    return routes
  }
  return routes.reduce((acc, route) => {
    if (!route || isAiFeatureRoute(route)) {
      return acc
    }
    const nextRoute = { ...route }
    if (Array.isArray(nextRoute.children) && nextRoute.children.length > 0) {
      nextRoute.children = filterAiFeatureRoutes(nextRoute.children, aiEnabled)
    }
    acc.push(nextRoute)
    return acc
  }, [])
}
function isAiFeatureRoute(route = {}) {
  const path = String(route.path || '').toLowerCase()
  const component = String(route.component || '').toLowerCase()
  const name = String(route.name || '').toLowerCase()
  const title = String(route?.meta?.title ?? route?.title ?? '')
  return (
    path.includes('chathome') ||
    component.includes('chathome') ||
    name.includes('chathome') ||
    title.includes('AI')
  )
}
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
  return asyncRouterMap.filter(route => {
    if (type && route.children) {
      route.children = filterChildren(route.children)
@@ -84,7 +119,7 @@
  })
}
function filterChildren(childrenMap, lastRouter = false) {
function filterChildren(childrenMap, lastRouter = false) {
  var children = []
  childrenMap.forEach(el => {
    el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
@@ -94,11 +129,11 @@
      children.push(el)
    }
  })
  return children
}
// åŠ¨æ€è·¯ç”±éåŽ†ï¼ŒéªŒè¯æ˜¯å¦å…·å¤‡æƒé™
export function filterDynamicRoutes(routes) {
  return children
}
// åŠ¨æ€è·¯ç”±éåŽ†ï¼ŒéªŒè¯æ˜¯å¦å…·å¤‡æƒé™
export function filterDynamicRoutes(routes) {
  const res = []
  routes.forEach(route => {
    if (route.permissions) {
src/store/modules/user.js
@@ -7,13 +7,14 @@
const useUserStore = defineStore(
  'user',
  {
    state: () => ({
      token: getToken(),
      id: '',
      name: '',
      avatar: '',
      roles: [],
      permissions: []
    state: () => ({
      token: getToken(),
      id: '',
      name: '',
      avatar: '',
      roles: [],
      permissions: [],
      aiEnabled: 0
    }),
    actions: {
      // ç™»å½•
@@ -58,29 +59,31 @@
            this.id = user.userId
            this.name = user.userName
            this.avatar = avatar
            this.currentFactoryName = user.currentFactoryName
            this.nickName = user.nickName
            this.roleName = user.roles[0].roleName
            this.currentDeptId = user.tenantId
            this.currentLoginTime = this.getCurrentTime()
            resolve(res)
          }).catch(error => {
            reject(error)
          })
        })
            this.currentFactoryName = user.currentFactoryName
            this.nickName = user.nickName
            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)
          })
        })
      },
      // é€€å‡ºç³»ç»Ÿ
      logOut() {
        return new Promise((resolve, reject) => {
          logout(this.token).then(() => {
            this.token = ''
            this.roles = []
            this.permissions = []
            removeToken()
            resolve()
          }).catch(error => {
            reject(error)
          })
            this.token = ''
            this.roles = []
            this.permissions = []
            this.aiEnabled = 0
            removeToken()
            resolve()
          }).catch(error => {
            reject(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 type="primary" @click="handelConfirm">确定</el-button>
     <el-button @click="close">取消</el-button>
    </template>
  </el-dialog>`
}
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
<template>
  <transition name="fade">
    <section v-if="visible" class="assistant-workspace">
      <div class="assistant-workspace__panel">
        <button
          v-if="assistantMode === 'pending'"
          type="button"
          class="workspace-back-btn"
          @click="$emit('close')"
        >
          <el-icon><ArrowLeftBold /></el-icon>
          <span>返回工业大屏</span>
        </button>
        <div class="assistant-workspace__body">
          <AIChatSidebar
            v-if="assistantMode !== 'pending'"
            :key="assistantMode"
            class="workspace-chat"
            :assistants="assistantMode === 'purchase' ? [purchaseAssistant] : [generalAssistant]"
            :default-assistant="assistantMode"
            :hide-trigger="true"
            :auto-open="true"
            drawer-size="100%"
            drawer-direction="ttb"
            header-extra-action-text="返回工业大屏"
            @header-extra-action="$emit('close')"
          />
          <div v-else class="workspace-pending">
            <div class="workspace-pending__content">
              <h3>{{ agentTitle }}</h3>
              <p>正在开发,敬请期待......</p>
            </div>
          </div>
        </div>
      </div>
    </section>
  </transition>
</template>
<script setup>
import { computed } from "vue";
import { ArrowLeftBold } from "@element-plus/icons-vue";
import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
import { generalAssistant, purchaseAssistant } from "@/components/AIChatSidebar/assistants";
const props = defineProps({
  visible: {
    type: Boolean,
    default: false,
  },
  agent: {
    type: Object,
    default: () => ({}),
  },
});
defineEmits(["close"]);
const agentKey = computed(() => String(props.agent?.key || ""));
const agentTitle = computed(() => String(props.agent?.name || "AI助手"));
const assistantMode = computed(() => {
  if (agentKey.value === "purchase") return "purchase";
  if (agentKey.value === "general") return "general";
  return "pending";
});
</script>
<style scoped>
.assistant-workspace {
  position: fixed;
  inset: 0;
  z-index: 2100;
  padding: 12px;
  background: rgba(33, 49, 63, 0.24);
  backdrop-filter: blur(2px);
}
.assistant-workspace__panel {
  position: relative;
  height: 100%;
  border-radius: 22px;
  border: 1px solid var(--surface-border);
  background: linear-gradient(180deg, #f9fcfb 0%, #f0f5f2 100%);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}
.assistant-workspace__body {
  height: 100%;
  min-height: 100%;
}
.workspace-back-btn {
  position: absolute;
  top: 16px;
  right: 20px;
  z-index: 5;
  height: 36px;
  padding: 0 14px;
  border: 1px solid rgba(38, 112, 183, 0.3);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.92);
  color: #25528f;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}
.workspace-back-btn:hover {
  border-color: rgba(31, 122, 114, 0.45);
  color: #1f5ddf;
  box-shadow: 0 8px 16px rgba(31, 122, 114, 0.14);
  transform: translateY(-1px);
}
.workspace-chat {
  width: 100%;
  height: 100%;
}
.workspace-chat :deep(.ai-chat-sidebar-wrapper) {
  height: 100%;
}
.workspace-chat :deep(.ai-chat-drawer) {
  height: 100%;
}
.workspace-chat :deep(.el-drawer) {
  height: 100% !important;
  width: 100% !important;
}
.workspace-pending {
  height: 100%;
  display: grid;
  place-items: center;
  padding: 20px;
  color: var(--text-secondary);
}
.workspace-pending__content {
  display: grid;
  gap: 12px;
  text-align: center;
}
.workspace-pending__content h3 {
  margin: 0;
  font-size: 36px;
  color: var(--text-primary);
}
.workspace-pending__content p {
  margin: 0;
  font-size: 24px;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
@media (max-width: 1600px) {
  .workspace-back-btn {
    top: 12px;
    right: 14px;
    height: 32px;
    padding: 0 12px;
    font-size: 13px;
  }
  .workspace-pending__content h3 {
    font-size: 30px;
  }
  .workspace-pending__content p {
    font-size: 20px;
  }
}
</style>
src/views/aiIndustrialBrain/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1499 @@
<template>
  <div ref="screenRef" class="ai-brain-screen">
    <section class="brain-stage">
      <header class="brain-head">
        <div class="head-date">
          <p>{{ weekLabel }}</p>
          <p>{{ dateLabel }}</p>
        </div>
        <div class="head-title">
          <span>AI工业大脑</span>
        </div>
        <div class="head-actions">
          <button type="button" class="head-back-btn" @click="goBack">
            <el-icon><ArrowLeftBold /></el-icon>
            <span>返回</span>
          </button>
        </div>
      </header>
      <section class="brain-intro">
        <h2>工业AI数字员工,赋能智造新纪元</h2>
        <p>六大AI助手协同企业管理、销售、采购、生产、财务及数据全链路</p>
        <div class="intro-sign">阿里云 Ã— åƒé—®å¤§æ¨¡åž‹ Ã— æ™ºèƒ½ä½“AI</div>
      </section>
      <section class="carousel-area">
        <button type="button" class="nav-btn nav-btn--left" @click="prevCard">
          <el-icon><ArrowLeftBold /></el-icon>
        </button>
        <div class="carousel-track">
          <article
            v-for="card in visibleCards"
            :key="card.agent.key"
            class="agent-card"
            :class="{ 'agent-card--active': card.offset === 0 }"
            :style="getCardStyle(card.offset)"
            @click="openAssistant(card.realIndex)"
          >
            <div class="agent-card__head" :class="{ 'agent-card__head--active': card.offset === 0 }">
              {{ card.agent.name }}
            </div>
            <div class="agent-card__body" :class="{ 'agent-card__body--active': card.offset === 0 }">
              <div class="avatar-shell" :class="{ 'avatar-shell--active': card.offset === 0 }">
                <div class="avatar-base"></div>
                <div class="avatar-cut">
                  <img v-if="card.agent.avatar" class="avatar-cut__img" :src="card.agent.avatar" :alt="card.agent.name" />
                </div>
              </div>
              <div v-if="card.offset === 0" class="highlight-list">
                <div
                  v-for="highlight in card.agent.highlights"
                  :key="highlight"
                  class="highlight-item"
                >
                  {{ highlight }}
                </div>
              </div>
            </div>
          </article>
        </div>
        <button type="button" class="nav-btn nav-btn--right" @click="nextCard">
          <el-icon><ArrowRightBold /></el-icon>
        </button>
      </section>
      <section class="brain-footer">
        <div class="footer-grid-overlay"></div>
        <div class="footer-metrics">
          <article class="footer-metric">
            <span class="footer-metric__label">在线智能体</span>
            <strong class="footer-metric__value">{{ agents.length }}个</strong>
            <small class="footer-metric__hint">全链路协同运行</small>
          </article>
          <article class="footer-metric footer-metric--focus">
            <span class="footer-metric__label">当前焦点</span>
            <strong class="footer-metric__value">{{ getFooterAgentName(focusAgent.name) }}</strong>
            <small class="footer-metric__hint">{{ focusAgent.highlights?.[0] || "智能分析联动" }}</small>
          </article>
          <article class="footer-metric footer-metric--period">
            <span class="footer-metric__label">轮播周期</span>
            <strong class="footer-metric__value">{{ carouselSecondsText }}</strong>
            <div class="footer-period-control">
              <button type="button" class="period-btn" @click="adjustCarouselSeconds(-0.5)">-</button>
              <input
                v-model.number="carouselSeconds"
                class="period-input"
                type="number"
                min="2"
                max="12"
                step="0.5"
              />
              <span class="period-unit">s</span>
              <button type="button" class="period-btn" @click="adjustCarouselSeconds(0.5)">+</button>
            </div>
            <input
              v-model.number="carouselSeconds"
              class="footer-period-slider"
              type="range"
              min="2"
              max="12"
              step="0.5"
            />
            <small class="footer-metric__hint">可手动设置 2.0s - 12.0s</small>
          </article>
        </div>
        <div class="footer-rail">
          <div class="footer-rail__line">
            <span class="footer-rail__flow"></span>
          </div>
          <div class="footer-rail__nodes">
            <button
              v-for="node in footerNodes"
              :key="node.key"
              type="button"
              class="footer-node"
              :class="{ 'footer-node--active': node.index === carouselIndex }"
              @click="openAssistant(node.index)"
            >
              <span class="footer-node__dot"></span>
              <span class="footer-node__name">{{ getFooterAgentName(node.name) }}</span>
            </button>
          </div>
        </div>
      </section>
    </section>
    <AiAssistantWorkspace
      :visible="fullscreenVisible"
      :agent="currentAgent"
      @close="closeFullscreen"
    />
  </div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { ArrowLeftBold, ArrowRightBold } from "@element-plus/icons-vue";
import AiAssistantWorkspace from "./components/AiAssistantWorkspace.vue";
import todoAvatar from "@/assets/AI/待办助手.png";
import salesAvatar from "@/assets/AI/销售助手.png";
import purchaseAvatar from "@/assets/AI/采购助手.png";
import productionAvatar from "@/assets/AI/生产助手.png";
import financeAvatar from "@/assets/AI/财务助手.png";
const router = useRouter();
const agents = [
  {
    key: "general",
    name: "AI待办助手",
    highlights: ["跨模块流程诊断", "经营风险智能提醒"],
  },
  {
    key: "sales",
    name: "AI销售助手",
    highlights: ["客户流失风险分析", "回款与报价策略建议"],
  },
  {
    key: "purchase",
    name: "AI采购助手",
    highlights: ["供应链指标分析", "采购订单智能生成"],
  },
  {
    key: "production",
    name: "AI生产助手",
    highlights: ["工序瓶颈定位", "产能与报废智能预警"],
  },
  {
    key: "finance",
    name: "AI财务助手",
    highlights: ["现金流压力预判", "费用结构智能分析"],
  },
];
const avatarByAgentKey = {
  general: todoAvatar,
  sales: salesAvatar,
  purchase: purchaseAvatar,
  production: productionAvatar,
  finance: financeAvatar,
};
for (let i = agents.length - 1; i >= 0; i -= 1) {
  const agent = agents[i];
  const avatar = avatarByAgentKey[agent.key];
  if (!avatar) {
    agents.splice(i, 1);
    continue;
  }
  agent.avatar = avatar;
}
const carouselIndex = ref(Math.min(2, Math.max(agents.length - 1, 0)));
const fullscreenVisible = ref(false);
const screenRef = ref(null);
const carouselIntervalMs = ref(4500);
let carouselTimer = null;
const fallbackAgent = {
  key: "fallback",
  name: "AI助手",
  avatar: "",
  highlights: [],
};
const currentAgent = computed(() => agents[carouselIndex.value] || agents[0] || fallbackAgent);
const focusAgent = computed(() => currentAgent.value || fallbackAgent);
const footerNodes = computed(() =>
  agents.map((agent, index) => ({
    key: agent.key,
    name: agent.name,
    index,
  }))
);
const carouselSeconds = computed({
  get: () => Number((carouselIntervalMs.value / 1000).toFixed(1)),
  set: (value) => {
    const next = Number(value);
    if (!Number.isFinite(next)) return;
    const clamped = Math.max(2, Math.min(12, Math.round(next * 2) / 2));
    carouselIntervalMs.value = Math.round(clamped * 1000);
  },
});
const carouselSecondsText = computed(() => `${carouselSeconds.value.toFixed(1)}s`);
const weekLabel = computed(() => {
  const weekMap = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
  return weekMap[new Date().getDay()];
});
const dateLabel = computed(() => {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  return `${year}å¹´${month}月${day}日`;
});
const visibleCards = computed(() => {
  const total = agents.length;
  return agents
    .map((agent, index) => {
      let offset = index - carouselIndex.value;
      if (offset > total / 2) offset -= total;
      if (offset < -total / 2) offset += total;
      return { agent, offset, realIndex: index };
    })
    .filter((item) => Math.abs(item.offset) <= 2)
    .sort((a, b) => a.offset - b.offset);
});
function getCardStyle(offset) {
  const distance = Math.abs(offset);
  const scale = distance === 0 ? 1 : distance === 1 ? 0.88 : 0.78;
  const opacity = distance === 0 ? 1 : distance === 1 ? 0.92 : 0.76;
  return {
    transform: `translateX(${offset * 340}px) scale(${scale})`,
    zIndex: String(50 - distance),
    opacity,
  };
}
function getFooterAgentName(name) {
  return String(name || "AI助手").replace(/^AI/, "");
}
function adjustCarouselSeconds(delta) {
  carouselSeconds.value = carouselSeconds.value + delta;
}
function prevCard() {
  const total = agents.length;
  if (!total) return;
  carouselIndex.value = (carouselIndex.value - 1 + total) % total;
}
function nextCard() {
  const total = agents.length;
  if (!total) return;
  carouselIndex.value = (carouselIndex.value + 1) % total;
}
async function enterBrowserFullscreen() {
  if (document.fullscreenElement) return;
  const target = screenRef.value || document.documentElement;
  if (!target || typeof target.requestFullscreen !== "function") return;
  try {
    await target.requestFullscreen();
  } catch (error) {
    // Ignore: browser may block fullscreen when there is no direct user activation.
  }
}
async function exitBrowserFullscreen() {
  if (!document.fullscreenElement || typeof document.exitFullscreen !== "function") return;
  try {
    await document.exitFullscreen();
  } catch (error) {
    // Ignore fullscreen exit failures.
  }
}
function goBack() {
  closeFullscreen();
  exitBrowserFullscreen();
  if (window.history.length > 1) {
    router.back();
    return;
  }
  router.push("/index");
}
function openAssistant(index) {
  if (!agents.length) return;
  carouselIndex.value = index;
  fullscreenVisible.value = true;
}
function closeFullscreen() {
  fullscreenVisible.value = false;
}
function startCarousel() {
  stopCarousel();
  if (fullscreenVisible.value) return;
  carouselTimer = window.setInterval(() => {
    nextCard();
  }, carouselIntervalMs.value);
}
function stopCarousel() {
  if (carouselTimer) {
    window.clearInterval(carouselTimer);
    carouselTimer = null;
  }
}
function handleEscClose(event) {
  if (event.key === "Escape" && fullscreenVisible.value) {
    closeFullscreen();
  }
}
watch(
  () => fullscreenVisible.value,
  (opened) => {
    if (opened) {
      stopCarousel();
    } else {
      startCarousel();
    }
  }
);
watch(
  () => carouselIntervalMs.value,
  () => {
    if (!fullscreenVisible.value) {
      startCarousel();
    }
  }
);
onMounted(() => {
  startCarousel();
  window.addEventListener("keydown", handleEscClose);
  window.requestAnimationFrame(() => {
    enterBrowserFullscreen();
  });
});
onBeforeUnmount(() => {
  stopCarousel();
  window.removeEventListener("keydown", handleEscClose);
  exitBrowserFullscreen();
});
</script>
<style scoped>
.ai-brain-screen {
  position: fixed;
  inset: 0;
  z-index: 1900;
  padding: 10px;
  overflow: hidden;
  background: var(--app-bg);
}
.brain-stage {
  position: relative;
  height: 100%;
  min-height: 100%;
  border-radius: 22px;
  border: 1px solid var(--surface-border);
  background:
    radial-gradient(circle at 14% 8%, rgba(31, 122, 114, 0.14), transparent 40%),
    radial-gradient(circle at 86% 12%, rgba(30, 91, 255, 0.1), transparent 42%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(245, 249, 247, 0.94)),
    repeating-linear-gradient(
      135deg,
      rgba(255, 255, 255, 0.05) 0,
      rgba(255, 255, 255, 0.05) 14px,
      rgba(31, 122, 114, 0.03) 14px,
      rgba(31, 122, 114, 0.03) 28px
    );
  box-shadow: var(--shadow-sm);
}
.brain-head {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr) 180px;
  align-items: center;
  padding: 12px 18px 0;
}
.head-date {
  color: var(--text-secondary);
  font-size: 24px;
  font-weight: 600;
}
.head-date p {
  margin: 0;
  line-height: 1.2;
}
.head-title {
  justify-self: center;
  width: min(760px, 95%);
  height: 68px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 0 0 46px 46px;
  color: #fff;
  font-size: 42px;
  font-style: italic;
  font-weight: 700;
  letter-spacing: 1px;
  background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
  box-shadow: 0 16px 30px rgba(31, 122, 114, 0.24);
}
.head-actions {
  justify-self: end;
}
.head-back-btn {
  height: 40px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: none;
  border-radius: 999px;
  font-size: 16px;
  font-weight: 600;
  color: var(--colorPrimary);
  background: var(--surface-base);
  box-shadow: 0 8px 18px rgba(31, 49, 38, 0.12);
  cursor: pointer;
}
.brain-intro {
  text-align: center;
  margin-top: 34px;
}
.brain-intro h2 {
  margin: 0;
  font-size: 44px;
  font-style: italic;
  font-weight: 700;
  color: var(--text-primary);
}
.brain-intro p {
  margin: 12px 0 10px;
  font-size: 28px;
  color: var(--text-secondary);
}
.intro-sign {
  display: inline-block;
  padding: 6px 18px;
  border-radius: 999px;
  font-size: 24px;
  font-weight: 700;
  color: #1e5bff;
  background: rgba(255, 255, 255, 0.82);
  border: 1px solid rgba(30, 91, 255, 0.18);
}
.carousel-area {
  position: relative;
  margin-top: 34px;
  padding: 0 72px 12px;
}
.carousel-track {
  position: relative;
  height: 500px;
  overflow: hidden;
}
.brain-footer {
  position: relative;
  margin: 0 72px;
  height: clamp(226px, 25vh,0);
  border-radius: 18px;
  border: 1px solid rgba(31, 122, 114, 0.28);
  background:
    linear-gradient(120deg, rgba(31, 122, 114, 0.14), rgba(30, 91, 255, 0.14)),
    linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(236, 244, 249, 0.9));
  box-shadow:
    0 16px 34px rgba(31, 81, 131, 0.12),
    inset 0 1px 0 rgba(255, 255, 255, 0.72);
  overflow: hidden;
}
.brain-footer::before {
  content: "";
  position: absolute;
  left: -22%;
  bottom: -120%;
  width: 52%;
  height: 260%;
  background: radial-gradient(ellipse at center, rgba(30, 91, 255, 0.2) 0%, rgba(30, 91, 255, 0) 72%);
  pointer-events: none;
}
.brain-footer::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(110deg, transparent 12%, rgba(255, 255, 255, 0.24) 38%, transparent 64%);
  transform: translateX(-120%);
  animation: footerSweep 5.8s linear infinite;
  pointer-events: none;
}
.footer-grid-overlay {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(
      90deg,
      rgba(31, 122, 114, 0.07) 0,
      rgba(31, 122, 114, 0.07) 1px,
      transparent 1px,
      transparent 36px
    ),
    repeating-linear-gradient(
      0deg,
      rgba(30, 91, 255, 0.06) 0,
      rgba(30, 91, 255, 0.06) 1px,
      transparent 1px,
      transparent 28px
    );
  opacity: 0.72;
  pointer-events: none;
}
.footer-metrics {
  position: relative;
  z-index: 2;
  padding: 14px 20px 72px;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
.footer-metric {
  min-height: 76px;
  border-radius: 12px;
  padding: 10px 14px;
  border: 1px solid rgba(37, 124, 188, 0.2);
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(245, 250, 255, 0.82));
  box-shadow: 0 10px 18px rgba(29, 83, 134, 0.08);
  display: grid;
  grid-template-rows: auto auto 1fr;
  gap: 4px;
}
.footer-metric--focus {
  border-color: rgba(38, 122, 194, 0.34);
  box-shadow:
    0 12px 22px rgba(30, 91, 255, 0.12),
    inset 0 0 0 1px rgba(85, 148, 232, 0.2);
}
.footer-metric__label {
  font-size: 14px;
  color: rgba(38, 72, 108, 0.88);
  font-weight: 600;
}
.footer-metric__value {
  font-size: 30px;
  line-height: 1;
  font-style: italic;
  font-weight: 700;
  color: #1f5ddf;
  text-shadow: 0 3px 10px rgba(30, 91, 255, 0.18);
}
.footer-metric__hint {
  margin-top: auto;
  font-size: 13px;
  color: rgba(52, 89, 128, 0.82);
}
.footer-metric--period .footer-metric__hint {
  margin-top: 0;
  line-height: 1.25;
}
.footer-metric--period {
  min-height: 122px;
  grid-template-rows: auto auto auto auto auto;
  gap: 6px;
}
.footer-period-control {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.period-btn {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid rgba(38, 112, 183, 0.28);
  background: rgba(255, 255, 255, 0.9);
  color: #2054c9;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.period-input {
  width: 56px;
  height: 24px;
  border-radius: 8px;
  border: 1px solid rgba(38, 112, 183, 0.24);
  background: rgba(255, 255, 255, 0.94);
  color: #1f5ddf;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  padding: 0 4px;
}
.period-unit {
  font-size: 12px;
  font-weight: 600;
  color: rgba(40, 80, 117, 0.86);
}
.footer-period-slider {
  width: min(250px, 100%);
  height: 3px;
  accent-color: #2a6ded;
  cursor: pointer;
}
.footer-rail {
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 18px;
  z-index: 2;
}
.footer-rail__line {
  position: relative;
  height: 2px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(31, 122, 114, 0.12), rgba(30, 91, 255, 0.6), rgba(31, 122, 114, 0.12));
  overflow: hidden;
}
.footer-rail__flow {
  position: absolute;
  top: -1px;
  left: -18%;
  width: 22%;
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(31, 122, 114, 0), rgba(59, 146, 244, 0.92), rgba(30, 91, 255, 0));
  filter: blur(0.2px);
  animation: railFlow 3.1s ease-in-out infinite;
}
.footer-rail__nodes {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px;
}
.footer-node {
  height: 34px;
  border: 1px solid rgba(38, 112, 183, 0.18);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.76);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: rgba(40, 80, 117, 0.92);
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.26s ease;
}
.footer-node:hover {
  transform: translateY(-1px);
  border-color: rgba(31, 122, 114, 0.34);
  box-shadow: 0 8px 14px rgba(31, 122, 114, 0.14);
}
.footer-node--active {
  color: #fff;
  border-color: transparent;
  background: linear-gradient(135deg, rgba(31, 122, 114, 0.94), rgba(30, 91, 255, 0.94));
  box-shadow: 0 10px 18px rgba(30, 91, 255, 0.28);
}
.footer-node__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(30, 91, 255, 0.72);
  box-shadow: 0 0 10px rgba(30, 91, 255, 0.52);
}
.footer-node--active .footer-node__dot {
  background: #fff;
  box-shadow: 0 0 12px rgba(255, 255, 255, 0.72);
  animation: nodePulse 1.4s ease-in-out infinite;
}
.agent-card {
  position: absolute;
  left: 50%;
  top: 0;
  width: 460px;
  margin-left: -230px;
  cursor: pointer;
  transform-origin: center bottom;
  transition: transform 0.35s ease, opacity 0.35s ease;
}
.agent-card__head {
  height: 56px;
  border-radius: 12px 12px 0 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28px;
  color: #fff;
  font-weight: 700;
  background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
}
.agent-card__head--active {
  box-shadow:
    0 12px 22px rgba(30, 91, 255, 0.26),
    inset 0 0 0 1px rgba(255, 255, 255, 0.28);
  position: relative;
}
.agent-card__head--active::after {
  content: "";
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 6px;
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.96), rgba(255, 255, 255, 0.22));
}
.agent-card__body {
  position: relative;
  height: 430px;
  border: 1px solid var(--surface-border-strong);
  border-top: none;
  border-radius: 0 0 20px 20px;
  background: rgba(255, 255, 255, 0.96);
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  isolation: isolate;
  box-shadow: 0 12px 24px rgba(31, 49, 38, 0.1);
}
.agent-card__body--active {
  background: linear-gradient(180deg, rgba(248, 252, 251, 0.96), rgba(225, 241, 250, 0.9));
  border-color: rgba(31, 122, 114, 0.35);
}
.agent-card__body--active::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(108deg, transparent 28%, rgba(255, 255, 255, 0.34) 50%, transparent 72%);
  transform: translateX(-125%);
  animation: bodySweep 3.6s linear infinite;
  pointer-events: none;
  z-index: 1;
}
.avatar-shell {
  position: relative;
  width: 248px;
  height: 430px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  --base-core: rgba(53, 143, 222, 0.4);
  --base-ring: rgba(39, 122, 201, 0.62);
  --base-glow: rgba(46, 133, 214, 0.28);
}
.avatar-shell::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -10px;
  width: 268px;
  height: 58px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(
    ellipse at center,
    rgba(55, 140, 219, 0.22) 0%,
    rgba(55, 140, 219, 0.11) 46%,
    rgba(55, 140, 219, 0) 74%
  );
  filter: blur(2.4px);
  animation: baseGlow 4.6s ease-in-out infinite;
  z-index: 1;
  pointer-events: none;
}
.avatar-shell::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 248px;
  height: 46px;
  transform: translateX(-50%);
  border-radius: 50%;
  border: 2px solid var(--base-ring);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.64),
    0 0 24px var(--base-glow);
  animation: basePulse 3.1s ease-in-out infinite;
  z-index: 4;
}
.avatar-base {
  position: absolute;
  left: 50%;
  bottom: 2px;
  width: 224px;
  height: 38px;
  transform: translateX(-50%);
  z-index: 2;
  pointer-events: none;
}
.avatar-base::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(
      ellipse at center,
      rgba(255, 255, 255, 0.96) 0%,
      rgba(255, 255, 255, 0.92) 36%,
      var(--base-core) 68%,
      rgba(38, 118, 195, 0.08) 100%
    );
  box-shadow:
    0 0 30px var(--base-core),
    0 0 10px rgba(255, 255, 255, 0.34) inset;
  z-index: 3;
}
.avatar-base::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 194px;
  height: 194px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background:
    conic-gradient(
      from 180deg,
      transparent 0deg,
      var(--base-ring) 48deg,
      transparent 112deg,
      var(--base-ring) 208deg,
      transparent 284deg,
      rgba(33, 114, 191, 0.48) 332deg,
      transparent 360deg
    );
  -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
  mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
  opacity: 0.62;
  animation: baseRotate 10.5s linear infinite;
  z-index: 2;
}
.avatar-shell--active {
  --base-core: rgba(50, 141, 217, 0.52);
  --base-ring: rgba(42, 127, 205, 0.76);
  --base-glow: rgba(38, 130, 211, 0.38);
}
.avatar-cut {
  position: relative;
  width: 220px;
  height: 430px;
  z-index: 6;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  filter: saturate(1.04) drop-shadow(0 14px 18px rgba(24, 44, 66, 0.14));
  transform-origin: center 82%;
  animation: avatarFloat 3.2s ease-in-out infinite;
}
.avatar-cut__img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
  display: block;
}
.agent-card--active .avatar-cut {
  animation-duration: 2.6s;
}
.highlight-list {
  position: absolute;
  right: 10px;
  top: 14px;
  display: grid;
  gap: 8px;
  width: 220px;
  z-index: 16;
}
.highlight-item {
  border-radius: 10px;
  padding: 8px 10px;
  font-size: 18px;
  line-height: 1.4;
  color: #fff;
  background: rgba(33, 49, 63, 0.92);
  box-shadow: 0 8px 16px rgba(21, 30, 40, 0.22);
}
.agent-card--active .highlight-item {
  background: rgba(31, 122, 114, 0.9);
}
.nav-btn {
  position: absolute;
  top: 212px;
  z-index: 80;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: none;
  font-size: 30px;
  color: var(--colorPrimary);
  background: var(--surface-base);
  box-shadow: 0 10px 20px rgba(31, 49, 38, 0.16);
  cursor: pointer;
}
.nav-btn--left {
  left: 14px;
}
.nav-btn--right {
  right: 14px;
}
.ai-fullscreen {
  position: fixed;
  inset: 0;
  z-index: 2100;
  padding: 12px;
  background: rgba(33, 49, 63, 0.24);
  backdrop-filter: blur(2px);
}
.ai-panel {
  height: 100%;
  border-radius: 22px;
  border: 1px solid var(--surface-border);
  background: linear-gradient(180deg, #f9fcfb 0%, #f0f5f2 100%);
  display: grid;
  grid-template-rows: 62px minmax(0, 1fr) 110px;
  box-shadow: var(--shadow-md);
}
.ai-panel__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
}
.ai-brand {
  font-size: 34px;
  color: var(--text-primary);
  font-weight: 700;
}
.ai-close {
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: transparent;
  font-size: 30px;
  color: var(--text-secondary);
  cursor: pointer;
}
.ai-panel__center {
  padding: 8px 20px 10px;
  display: grid;
  grid-template-rows: 120px 290px minmax(0, 1fr);
  gap: 10px;
  min-height: 0;
}
.welcome-card {
  border-radius: 14px;
  background: linear-gradient(135deg, rgba(232, 244, 242, 0.95), rgba(230, 237, 250, 0.9));
  padding: 16px 18px;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  border: 1px solid var(--surface-border);
}
.welcome-card__text h3 {
  margin: 0;
  font-size: 28px;
  color: var(--text-primary);
}
.welcome-card__text p {
  margin: 8px 0 0;
  font-size: 20px;
  color: var(--text-secondary);
}
.mini-avatar {
  width: 120px;
  height: 120px;
  border-radius: 14px;
  border: 1px solid var(--surface-border);
  background-color: #fff;
  background-clip: border-box;
  overflow: hidden;
}
.mini-avatar__img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
  display: block;
}
.recommend-card {
  border-radius: 14px;
  border: 1px solid var(--surface-border);
  background: rgba(255, 255, 255, 0.86);
  padding: 12px 14px;
}
.recommend-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  color: var(--text-primary);
  font-size: 24px;
  font-weight: 700;
}
.refresh-btn {
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: 18px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
}
.recommend-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px 18px;
}
.recommend-item {
  border: 1px solid var(--surface-border);
  border-radius: 8px;
  text-align: left;
  padding: 8px 10px;
  font-size: 18px;
  color: var(--text-secondary);
  background: #fff;
  cursor: pointer;
}
.recommend-item:hover {
  background: rgba(31, 122, 114, 0.08);
  color: var(--colorPrimary);
}
.chat-card {
  border-radius: 14px;
  border: 1px solid var(--surface-border);
  background: #fff;
  min-height: 0;
  overflow: hidden;
}
.chat-empty {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-tertiary);
  font-size: 18px;
}
.chat-messages {
  height: 100%;
  overflow-y: auto;
  padding: 14px;
  display: grid;
  gap: 10px;
}
.chat-row {
  display: flex;
}
.chat-row--assistant {
  justify-content: flex-start;
}
.chat-row--user {
  justify-content: flex-end;
}
.chat-bubble {
  max-width: 72%;
  border-radius: 12px;
  padding: 10px 12px;
  font-size: 18px;
  line-height: 1.5;
  white-space: pre-wrap;
  color: var(--text-primary);
  background: var(--surface-soft);
  border: 1px solid var(--surface-border);
}
.chat-row--user .chat-bubble {
  color: #fff;
  background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
  border: none;
}
.ai-panel__input {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 130px;
  gap: 12px;
  padding: 14px 20px 18px;
}
.ask-input :deep(.el-input__wrapper) {
  height: 74px;
  border-radius: 18px;
  box-shadow: 0 0 0 1px var(--surface-border) inset;
  background: #fff;
}
.ask-input :deep(.el-input__inner) {
  font-size: 20px;
}
.send-btn {
  align-self: center;
  height: 56px;
  font-size: 20px;
  min-width: 98px;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
@keyframes avatarFloat {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-8px);
  }
}
@keyframes basePulse {
  0%,
  100% {
    transform: translateX(-50%) scale(1);
    opacity: 0.88;
  }
  50% {
    transform: translateX(-50%) scale(1.045);
    opacity: 0.95;
  }
}
@keyframes baseRotate {
  from {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  to {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}
@keyframes baseGlow {
  0%,
  100% {
    transform: translateX(-50%) scaleX(1);
    opacity: 0.84;
  }
  50% {
    transform: translateX(-50%) scaleX(1.06);
    opacity: 0.96;
  }
}
@keyframes bodySweep {
  0% {
    transform: translateX(-125%);
  }
  100% {
    transform: translateX(135%);
  }
}
@keyframes footerSweep {
  0% {
    transform: translateX(-120%);
  }
  100% {
    transform: translateX(140%);
  }
}
@keyframes railFlow {
  0% {
    transform: translateX(0);
    opacity: 0;
  }
  20% {
    opacity: 1;
  }
  80% {
    opacity: 1;
  }
  100% {
    transform: translateX(520%);
    opacity: 0;
  }
}
@keyframes nodePulse {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.25);
  }
}
@media (max-width: 1600px) {
  .head-title {
    font-size: 34px;
    height: 60px;
  }
  .brain-intro h2 {
    font-size: 36px;
  }
  .brain-intro p {
    font-size: 22px;
  }
  .intro-sign {
    font-size: 20px;
  }
  .agent-card {
    width: 380px;
    margin-left: -190px;
  }
  .agent-card__head {
    font-size: 24px;
    height: 54px;
  }
  .agent-card__body {
    height: 390px;
  }
  .highlight-list {
    width: 184px;
  }
  .highlight-item {
    font-size: 15px;
  }
  .avatar-shell {
    width: 220px;
    height: 390px;
  }
  .avatar-cut {
    width: 202px;
    height: 390px;
  }
  .avatar-base {
    width: 194px;
    height: 34px;
  }
  .avatar-base::after {
    width: 164px;
    height: 164px;
  }
  .avatar-shell::before {
    width: 236px;
    height: 48px;
    bottom: -9px;
  }
  .avatar-shell::after {
    width: 220px;
    height: 40px;
    bottom: 0;
  }
  .brain-footer {
    margin: 0 52px;
    height: clamp(210px, 23vh, 264px);
  }
  .footer-metrics {
    padding: 12px 14px 66px;
    gap: 8px;
  }
  .footer-metric {
    min-height: 66px;
    padding: 8px 10px;
  }
  .footer-metric--period {
    min-height: 108px;
    gap: 4px;
  }
  .footer-metric__label {
    font-size: 12px;
  }
  .footer-metric__value {
    font-size: 24px;
  }
  .footer-metric__hint {
    font-size: 11px;
  }
  .footer-period-control {
    gap: 6px;
  }
  .period-btn {
    width: 20px;
    height: 20px;
    font-size: 12px;
  }
  .period-input {
    width: 50px;
    height: 22px;
    font-size: 12px;
  }
  .footer-period-slider {
    width: 100%;
  }
  .footer-rail {
    left: 14px;
    right: 14px;
    bottom: 14px;
  }
  .footer-rail__nodes {
    margin-top: 10px;
    gap: 6px;
  }
  .footer-node {
    height: 30px;
    font-size: 12px;
    gap: 6px;
  }
  .footer-node__dot {
    width: 7px;
    height: 7px;
  }
  .ai-brand {
    font-size: 28px;
  }
  .welcome-card__text h3,
  .recommend-card__head {
    font-size: 22px;
  }
  .welcome-card__text p,
  .recommend-item,
  .chat-bubble,
  .refresh-btn,
  .chat-empty,
  .ask-input :deep(.el-input__inner),
  .send-btn {
    font-size: 16px;
  }
}
</style>
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"
@@ -29,6 +29,9 @@
      <div>
        <el-button type="primary"
                   @click="openForm('add')">新增客户</el-button>
        <el-button type="primary"
                   plain
                   @click="back">流入公海</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="info"
                   plain
@@ -159,7 +162,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"
@@ -562,19 +566,13 @@
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
  import {
    addCustomerPrivate,
    delCustomerPrivate,
    getCustomer,
    getCustomerPrivatePoolById,
    getCustomerPrivatePoolInfo,
    listCustomerPrivatePool,
    updateCustomerPrivatePool,
    addCustomerFollow,
    updateCustomerFollow,
    delCustomerFollow,
    addReturnVisit,
    getReturnVisit,
  } from "@/api/basicData/customerFile.js";
  import {listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer, backCustomer} from "@/api/basicData/customer.js";
  import { ElMessageBox } from "element-plus";
  import { userListNoPage } from "@/api/system/user.js";
  import useUserStore from "@/store/modules/user";
@@ -606,7 +604,7 @@
  const negotiationFormRef = ref();
  const negotiationForm = reactive({
    customerName: "",
        customerPrivatePoolId: "",
        customerId: "",
    followUpMethod: "",
    followUpLevel: "",
    followUpTime: "",
@@ -769,6 +767,7 @@
    searchForm: {
      customerName: "",
      customerType: "",
      type: 0
    },
    form: {
      customerName: "",
@@ -778,6 +777,7 @@
      contactPhone: "",
      contactPosition: "",
      customerType: "",
      type: 0
    },
    rules: {
      customerName: [{ required: true, message: "请输入", trigger: "blur" }],
@@ -796,7 +796,10 @@
    // è®¾ç½®ä¸Šä¼ çš„请求头部
    headers: { Authorization: "Bearer " + getToken() },
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "/customerPrivate/importData",
    url: import.meta.env.VITE_APP_BASE_API + "/basic/customer/importData",
    data: {
      type: 0
    },
    // æ–‡ä»¶ä¸Šä¼ å‰çš„回调
    beforeUpload: file => {
      console.log("文件即将上传", file);
@@ -868,7 +871,7 @@
  };
  const getList = () => {
    tableLoading.value = true;
    listCustomerPrivatePool({ ...searchForm.value, ...page }).then(res => {
    listCustomer({ ...searchForm.value, ...page }).then(res => {
      tableLoading.value = false;
      tableData.value = res.data.records;
      page.total = res.data.total;
@@ -890,7 +893,7 @@
  }
  /** ä¸‹è½½æ¨¡æ¿ */
  function importTemplate() {
    proxy.download("/customerPrivate/downloadTemplate", {}, "客户导入模板.xlsx");
    proxy.download("/basic/customer/downloadTemplate", {}, "客户导入模板.xlsx");
  }
  // æ‰“开弹框
  const openForm = (type, row) => {
@@ -903,11 +906,13 @@
        contactPosition: "",
      },
    ];
    form.value.type = 0;
    form.value.maintenanceTime = getCurrentDate();
    userListNoPage().then(res => {
      userList.value = res.data;
    });
    if (type === "edit") {
      getCustomerPrivatePoolById(row.id).then(res => {
      getCustomer(row.id).then(res => {
        form.value = { ...res.data };
        const persons = String(res.data.contactPerson || "").split(",");
        const phones = String(res.data.contactPhone || "").split(",");
@@ -951,6 +956,7 @@
    form.value.contactPhone = formYYs.value.contactList
      .map(item => item.contactPhone)
      .join(",");
    addCustomer(form.value).then(res => {
    form.value.contactPosition = formYYs.value.contactList
      .map(item => item.contactPosition || "")
      .join(",");
@@ -971,7 +977,7 @@
    form.value.contactPosition = formYYs.value.contactList
      .map(item => item.contactPosition || "")
      .join(",");
    updateCustomerPrivatePool(form.value).then(res => {
    updateCustomer(form.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
      getList();
@@ -990,7 +996,7 @@
      type: "warning",
    })
      .then(() => {
        proxy.download("/customerPrivate/export", {}, "客户档案.xlsx");
        proxy.download("/basic/customer/export", {type: 0}, "客户档案.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -1019,7 +1025,7 @@
    })
      .then(() => {
        tableLoading.value = true;
        delCustomerPrivate(ids)
        delCustomer(ids)
          .then(() => {
            proxy.$modal.msgSuccess("删除成功");
            getList();
@@ -1030,6 +1036,36 @@
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  const back = () => {
    if (selectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    const ids = selectedRows.value.map(item => item.id);
    ElMessageBox.confirm("选中的客户将流入公海,是否确认?", "流入公海提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        tableLoading.value = true;
        return Promise.all(ids.map(id => backCustomer(id)))
          .then(() => {
            proxy.$modal.msgSuccess("流入公海成功");
            selectedRows.value = [];
            getList();
          })
          .finally(() => {
            tableLoading.value = false;
          });
      })
      .catch(error => {
        if (error === "cancel" || error === "close") {
          proxy.$modal.msg("已取消");
        }
      });
  };
@@ -1073,8 +1109,7 @@
        if (reminderForm.id) {
          submitvalue.value = {
            id: reminderForm.id,
                        customerPrivatePoolId: reminderForm.id,
                        customerPrivatePoolId: currentCustomerId.value,
                        customerId: currentCustomerId.value,
            isEnabled: reminderForm.reminderSwitch ? 1 : 0,
            content: reminderForm.reminderContent,
            reminderTime: reminderForm.reminderTime,
@@ -1082,7 +1117,7 @@
          };
        } else {
          submitvalue.value = {
                        customerPrivatePoolId: currentCustomerId.value,
            customerId: currentCustomerId.value,
            isEnabled: reminderForm.reminderSwitch ? 1 : 0,
            content: reminderForm.reminderContent,
            reminderTime: reminderForm.reminderTime,
@@ -1111,7 +1146,7 @@
  // æ‰“开洽谈进度弹窗
  const openNegotiationDialog = row => {
    negotiationForm.customerName = row.customerName;
    negotiationForm.customerPrivatePoolId = row.id;
    negotiationForm.customerId = row.id;
    negotiationForm.followUpMethod = "";
    negotiationForm.followUpLevel = "";
    negotiationForm.followUpTime = "";
@@ -1140,7 +1175,7 @@
          // ä¿®æ”¹æ“ä½œ
          updateCustomerFollow(negotiationForm).then(res => {
            // æ›´æ–°æœ¬åœ°æ•°æ®
            getCustomer(negotiationForm.customerPrivatePoolId).then(res => {
            getCustomer(negotiationForm.customerId).then(res => {
              // æ›´æ–°æœ¬åœ°æ•°æ®
              negotiationRecords.value = res.data.followUpList || [];
            });
@@ -1172,7 +1207,7 @@
  // æ‰“开详情弹窗
  const openDetailDialog = row => {
    getCustomerPrivatePoolInfo(row.id).then(res => {
    getCustomer(row.id).then(res => {
      // å¡«å……客户基本信息
      Object.assign(detailForm, res.data);
@@ -1193,7 +1228,7 @@
    // å°†å½“前记录数据填充到表单
    Object.assign(negotiationForm, {
      customerName: row.customerName,
            customerPrivatePoolId: row.customerPrivatePoolId,
            customerId: row.customerId,
      followUpMethod: row.followUpMethod,
      followUpLevel: row.followUpLevel,
      followUpTime: row.followUpTime,
@@ -1221,7 +1256,7 @@
        // });
        delCustomerFollow(row.id).then(() => {
          // åˆ é™¤æˆåŠŸåŽæ›´æ–°æœ¬åœ°æ•°æ®
          getCustomer(row.customerPrivatePoolId).then(res => {
          getCustomer(row.customerId).then(res => {
            // æ›´æ–°æœ¬åœ°æ•°æ®
            negotiationRecords.value = res.data.followUpList || [];
          });
@@ -1321,7 +1356,7 @@
  const downloadAttachment = row => {
    if (row.url) {
      // proxy.download(row.url, {}, row.name);
      proxy.$download.name(row.url);
            proxy.$download.byUrl(row.url, row.originalFilename);
    } else {
      proxy.$modal.msgError("下载链接不存在");
    }
src/views/basicData/customerFileOpenSea/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"
@@ -228,7 +228,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"
@@ -631,20 +632,23 @@
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
  import {
    addCustomer,
    addCustomerPrivatePool,
    delCustomerPrivatePool,
    delCustomer,
    getCustomer,
    shareCustomer,
    listCustomer,
    updateCustomer,
    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";
@@ -840,7 +844,7 @@
          type: "text",
          showHide: row => row.usageStatus == 1,
          clickFun: row => {
            recycleCustomer(row);
            recycle(row);
          },
        },
                {
@@ -896,6 +900,7 @@
    searchForm: {
      customerName: "",
      customerType: "",
      type: 1
    },
    form: {
      customerName: "",
@@ -905,6 +910,7 @@
      contactPhone: "",
      contactPosition: "",
      customerType: "",
      type: 1
    },
    rules: {
      customerName: [{ required: true, message: "请输入", trigger: "blur" }],
@@ -924,6 +930,9 @@
    headers: { Authorization: "Bearer " + getToken() },
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "/basic/customer/importData",
    data: {
      type: 1
    },
    // æ–‡ä»¶ä¸Šä¼ å‰çš„回调
    beforeUpload: file => {
      console.log("文件即将上传", file);
@@ -1136,7 +1145,7 @@
  const openShareDialog = row => {
    shareForm.id = row.id;
    shareForm.customerName = row.customerName;
    shareForm.boundIds = [];
    shareForm.boundIds = row.userIds || [];
    ensureUserList().then(() => {
      shareDialogVisible.value = true;
    });
@@ -1153,9 +1162,9 @@
      if (!valid) {
        return;
      }
      addCustomerPrivatePool({
        customerId: assignForm.id,
        boundId: assignForm.boundId,
      assignCustomer({
        id: assignForm.id,
        usageUser: assignForm.boundId,
      }).then(() => {
        proxy.$modal.msgSuccess("分配成功");
        closeAssignDialog();
@@ -1169,8 +1178,8 @@
        return;
      }
      shareCustomer({
        customerId: shareForm.id,
        boundIds: shareForm.boundIds,
        id: shareForm.id,
        userIds: shareForm.boundIds,
      }).then(() => {
        proxy.$modal.msgSuccess("共享成功");
        closeShareDialog();
@@ -1178,18 +1187,17 @@
      });
    });
  };
  const recycleCustomer = row => {
  const recycle = row => {
    ElMessageBox.confirm("确认回收客户“" + row.customerName + "”吗?", "回收提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        return delCustomerPrivatePool(row.id);
      })
      .then(() => {
        proxy.$modal.msgSuccess("回收成功");
        getList();
        return recycleCustomer({id: row.id}).then(() => {
          proxy.$modal.msgSuccess("回收成功");
          getList();
        })
      })
      .catch(error => {
        if (error === "cancel" || error === "close") {
@@ -1205,7 +1213,7 @@
      type: "warning",
    })
      .then(() => {
        proxy.download("/basic/customer/export", {}, "客户档案.xlsx");
        proxy.download("/basic/customer/export", {type: 1}, "客户档案.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -1564,7 +1572,7 @@
  const downloadAttachment = row => {
    if (row.url) {
      // proxy.download(row.url, {}, row.name);
      proxy.$download.name(row.url);
            proxy.$download.byUrl(row.url, row.originalFilename);
    } else {
      proxy.$modal.msgError("下载链接不存在");
    }
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/index.vue
@@ -2,41 +2,34 @@
  <div class="app-container product-view">
    <div class="left">
      <div>
        <el-input
          v-model="search"
          style="width: 210px"
          placeholder="输入关键字进行搜索"
          @change="searchFilter"
          @clear="searchFilter"
          clearable
          prefix-icon="Search"
        />
        <el-button
          v-if="false"
          type="primary"
          @click="openProDia('addOne')"
          style="margin-left: 10px"
          >新增产品大类</el-button
        >
        <el-input v-model="search"
                  style="width: 210px"
                  placeholder="输入关键字进行搜索"
                  @change="searchFilter"
                  @clear="searchFilter"
                  clearable
                  prefix-icon="Search" />
        <el-button v-if="false"
                   type="primary"
                   @click="openProDia('addOne')"
                   style="margin-left: 10px">新增产品大类</el-button>
      </div>
      <div ref="containerRef">
        <el-tree
          ref="tree"
          v-loading="treeLoad"
          :data="list"
          @node-click="handleNodeClick"
          :expand-on-click-node="false"
          @node-expand="handleNodeExpand"
          @node-collapse="handleNodeCollapse"
          :key="treeKey"
          :default-expanded-keys="expandedKeys"
          :filter-node-method="filterNode"
          :props="{ children: 'children', label: 'label' }"
          highlight-current
          node-key="id"
          class="product-tree-scroll"
          style="height: calc(100vh - 190px); overflow-y: auto"
        >
        <el-tree ref="tree"
                 v-loading="treeLoad"
                 :data="list"
                 @node-click="handleNodeClick"
                 :expand-on-click-node="false"
                 @node-expand="handleNodeExpand"
                 @node-collapse="handleNodeCollapse"
                 :key="treeKey"
                 :default-expanded-keys="expandedKeys"
                 :filter-node-method="filterNode"
                 :props="{ children: 'children', label: 'label' }"
                 highlight-current
                 node-key="id"
                 class="product-tree-scroll"
                 style="height: calc(100vh - 190px); overflow-y: auto">
          <template #default="{ node, data }">
            <div class="custom-tree-node">
              <span class="tree-node-content">
@@ -47,25 +40,23 @@
                <span class="tree-node-label">{{ data.label }}</span>
              </span>
              <div>
                <el-button
                  type="primary"
                  link
                  :disabled="isTopLevelNode(data, node)"
                  @click="openProDia('edit', data)"
                >
                <el-button type="primary"
                           link
                           :disabled="isTopLevelNode(data, node)"
                           @click="openProDia('edit', data, node)">
                  ç¼–辑
                </el-button>
                <el-button type="primary" link @click="openProDia('add', data)">
                <el-button type="primary"
                           link
                           @click="openProDia('add', data, node)">
                  æ·»åŠ äº§å“
                </el-button>
                <el-button
                  v-if="!node.childNodes.length"
                  style="margin-left: 4px"
                  type="danger"
                  link
                  :disabled="isTopLevelNode(data, node)"
                  @click="remove(node, data)"
                >
                <el-button v-if="!node.childNodes.length"
                           style="margin-left: 4px"
                           type="danger"
                           link
                           :disabled="isTopLevelNode(data, node)"
                           @click="remove(node, data)">
                  åˆ é™¤
                </el-button>
              </div>
@@ -79,99 +70,103 @@
        <el-button type="primary" @click="openModelDia('add')">
          æ–°å¢žå°ºå¯¸
        </el-button>
        <ImportExcel :product-id="currentId" @uploadSuccess="getModelList" />
        <el-button
          type="danger"
          @click="handleDelete"
          style="margin-left: 10px"
          plain
        >
        <ImportExcel :product-id="currentId"
                     @uploadSuccess="getModelList" />
        <el-button type="danger"
                   @click="handleDelete"
                   style="margin-left: 10px"
                   plain>
          åˆ é™¤
        </el-button>
      </div>
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
      ></PIMTable>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
    </div>
    <el-dialog v-model="productDia" title="产品" width="400px" @keydown.enter.prevent>
      <el-form
        :model="form"
        label-width="140px"
        label-position="top"
        :rules="rules"
        ref="formRef"
      >
    <el-dialog v-model="productDia"
               title="产品"
               width="400px"
               @keydown.enter.prevent>
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="产品名称:" prop="productName">
              <el-input
                v-model="form.productName"
                placeholder="请输入产品名称"
                maxlength="20"
                show-word-limit
                clearable
                @keydown.enter.prevent
              />
            <el-form-item label="产品名称:"
                          prop="productName">
              <el-input v-model="form.productName"
                        placeholder="请输入产品名称"
                        maxlength="20"
                        show-word-limit
                        clearable
                        @keydown.enter.prevent />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeProDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog
      v-model="modelDia"
      title="尺寸"
      width="400px"
      @close="closeModelDia"
      @keydown.enter.prevent
    >
      <el-form
        :model="modelForm"
        label-width="140px"
        label-position="top"
        :rules="modelRules"
        ref="modelFormRef"
      >
    <el-dialog v-model="modelDia"
               title="尺寸"
               width="400px"
               @close="closeModelDia"
               @keydown.enter.prevent>
      <el-form :model="modelForm"
               label-width="140px"
               label-position="top"
               :rules="modelRules"
               ref="modelFormRef">
        <el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="产品编号:"
                            prop="productCode">
                <el-input v-model="modelForm.productCode"
                          placeholder="请输入产品编号"
                          clearable
                          @keydown.enter.prevent />
              </el-form-item>
            </el-col>
          </el-row>
          <el-col :span="24">
            <el-form-item label="尺寸:" prop="model">
              <el-input
                v-model="modelForm.model"
                placeholder="请输入尺寸"
                clearable
                @keydown.enter.prevent
              />
            <el-form-item label="尺寸:"
                          prop="model">
              <el-input v-model="modelForm.model"
                        placeholder="请输入尺寸"
                        clearable
                        @keydown.enter.prevent />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="单位:" prop="unit">
              <el-input
                v-model="modelForm.unit"
                placeholder="请输入单位"
                clearable
                @keydown.enter.prevent
              />
            <el-form-item label="单位:"
                          prop="unit">
              <el-input v-model="modelForm.unit"
                        placeholder="请输入单位"
                        clearable
                        @keydown.enter.prevent />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitModelForm">确认</el-button>
          <el-button type="primary"
                     @click="submitModelForm">确认</el-button>
          <el-button @click="closeModelDia">取消</el-button>
        </div>
      </template>
@@ -180,473 +175,496 @@
</template>
<script setup>
import { nextTick, ref } from "vue";
import { ElMessageBox } from "element-plus";
import {
  addOrEditProduct,
  addOrEditProductModel,
  delProduct,
  delProductModel,
  modelListPage,
  productTreeList,
} from "@/api/basicData/product.js";
import ImportExcel from "./ImportExcel/index.vue";
  import { nextTick, ref } from "vue";
  import { ElMessageBox } from "element-plus";
  import {
    addOrEditProduct,
    addOrEditProductModel,
    delProduct,
    delProductModel,
    modelListPage,
    productTreeList,
  } from "@/api/basicData/product.js";
  import ImportExcel from "./ImportExcel/index.vue";
const { proxy } = getCurrentInstance();
const tree = ref(null);
const containerRef = ref(null);
const treeKey = ref(0);
const expandedKeySet = new Set();
const EXPANDED_STORAGE_KEY = "basicData_product_tree_expanded_keys_v2";
  const { 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;
  const loadExpandedKeys = () => {
    if (typeof window === "undefined") {
      return [];
    }
    let currentId = key;
    while (parentMap.has(currentId)) {
      const parentId = parentMap.get(currentId);
      if (!parentId) {
        return true;
      }
      if (!expandedKeySet.has(parentId)) {
    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;
      }
      currentId = parentId;
    }
    return true;
  });
  if (normalizedKeys.length !== expandedKeySet.size) {
    expandedKeySet.clear();
    normalizedKeys.forEach((key) => expandedKeySet.add(key));
    saveExpandedKeys();
  }
};
const productDia = ref(false);
const modelDia = ref(false);
const modelOperationType = ref("");
const search = ref("");
const currentId = ref("");
const currentParentId = ref("");
const operationType = ref("");
const treeLoad = ref(false);
const list = ref([]);
const expandedKeys = ref([]);
const tableColumn = ref([
  {
    label: "尺寸",
    prop: "model",
  },
  {
    label: "单位",
    prop: "unit",
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          openModelDia("edit", row);
        },
      },
    ],
  },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const isShowButton = ref(false);
const selectedRows = ref([]);
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
const data = reactive({
  form: {
    productName: "",
  },
  rules: {
    productName: [
      { required: true, message: "请输入", trigger: "blur" },
      { max: 20, message: "产品名称不能超过20个字符", trigger: "blur" },
    ],
  },
  modelForm: {
    model: "",
    unit: "",
  },
  modelRules: {
    model: [{ required: true, message: "请输入", trigger: "blur" }],
    unit: [{ required: true, message: "请输入", trigger: "blur" }],
  },
});
const { form, rules, modelForm, modelRules } = toRefs(data);
// æŸ¥è¯¢äº§å“æ ‘
const getProductTreeList = () => {
  treeLoad.value = true;
  productTreeList()
    .then((res) => {
      list.value = res || [];
      normalizeExpandedKeys(list.value);
      expandedKeys.value = Array.from(expandedKeySet);
      treeKey.value += 1;
      nextTick(() => {
        tree.value?.setDefaultExpandedKeys?.(expandedKeys.value);
      });
    })
    .catch((err) => {
      console.error(err);
    })
    .finally(() => {
      treeLoad.value = false;
    });
};
const handleNodeExpand = (data) => {
  nextTick(syncExpandedKeysFromTree);
};
const handleNodeCollapse = (data, node) => {
  node?.eachNode?.((item) => {
    item.collapse();
  });
  nextTick(syncExpandedKeysFromTree);
};
// è¿‡æ»¤äº§å“æ ‘
const searchFilter = () => {
  proxy.$refs.tree.filter(search.value);
};
const isTopLevelNode = (data, node) => {
  if (node?.level !== undefined) {
    return node.level === 1;
  }
  return [null, undefined, "", 0, "0"].includes(data?.parentId);
};
// æ‰“开产品弹框
const openProDia = (type, data) => {
  if (data && type === "edit" && isTopLevelNode(data)) {
    proxy.$modal.msgWarning("一级节点不能编辑或删除");
    return;
  }
  operationType.value = type;
  productDia.value = true;
  form.value.productName = "";
  if (type === "edit") {
    form.value.productName = data.productName;
  }
};
// æ‰“开尺寸弹框
const openModelDia = (type, data) => {
  modelOperationType.value = type;
  modelDia.value = true;
  modelForm.value.model = "";
  modelForm.value.model = "";
  modelForm.value.id = "";
  if (type === "edit") {
    modelForm.value = { ...data };
  }
};
// æäº¤äº§å“åç§°ä¿®æ”¹
const submitForm = () => {
  proxy.$refs.formRef.validate((valid) => {
    if (valid) {
      if (operationType.value === "add") {
        form.value.parentId = currentId.value;
        form.value.id = "";
      } else if (operationType.value === "addOne") {
        form.value.id = "";
        form.value.parentId = "";
      } else {
        form.value.id = currentId.value;
        form.value.parentId = "";
      let currentId = key;
      while (parentMap.has(currentId)) {
        const parentId = parentMap.get(currentId);
        if (!parentId) {
          return true;
        }
        if (!expandedKeySet.has(parentId)) {
          return false;
        }
        currentId = parentId;
      }
      addOrEditProduct(form.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeProDia();
        getProductTreeList();
      });
    }
  });
};
// å…³é—­äº§å“å¼¹æ¡†
const closeProDia = () => {
  proxy.$refs.formRef.resetFields();
  productDia.value = false;
};
      return true;
    });
// åˆ é™¤äº§å“
const remove = (node, data) => {
  if (isTopLevelNode(data, node)) {
    proxy.$modal.msgWarning("一级节点不能编辑或删除");
    return;
  }
  let ids = [];
  ids.push(data.id);
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      tableLoading.value = true;
      delProduct(ids)
        .then((res) => {
          proxy.$modal.msgSuccess("删除成功");
    if (normalizedKeys.length !== expandedKeySet.size) {
      expandedKeySet.clear();
      normalizedKeys.forEach(key => expandedKeySet.add(key));
      saveExpandedKeys();
    }
  };
  const productDia = ref(false);
  const modelDia = ref(false);
  const modelOperationType = ref("");
  const search = ref("");
  const currentId = ref("");
  const currentParentId = ref("");
  /** äº§å“å¼¹çª—:add å­˜çˆ¶èŠ‚ç‚¹ id;edit å­˜å½“前节点 id ä¸Ž parentId(不依赖树选中项) */
  const productDialogTarget = ref(null);
  const operationType = ref("");
  const treeLoad = ref(false);
  const list = ref([]);
  const expandedKeys = ref([]);
  const tableColumn = ref([
    {
      label: "产品编号",
      prop: "productCode",
    },
    {
      label: "尺寸",
      prop: "model",
    },
    {
      label: "单位",
      prop: "unit",
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      operation: [
        {
          name: "编辑",
          type: "text",
          clickFun: row => {
            openModelDia("edit", row);
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const tableLoading = ref(false);
  const isShowButton = ref(false);
  const selectedRows = ref([]);
  const page = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  const data = reactive({
    form: {
      productName: "",
    },
    rules: {
      productName: [
        { required: true, message: "请输入", trigger: "blur" },
        { max: 20, message: "产品名称不能超过20个字符", trigger: "blur" },
      ],
    },
    modelForm: {
      model: "",
      unit: "",
      productCode: "",
    },
    modelRules: {
      model: [{ required: true, message: "请输入", trigger: "blur" }],
      unit: [{ required: true, message: "请输入", trigger: "blur" }],
      productCode: [{ required: true, message: "请输入", trigger: "blur" }],
    },
  });
  const { form, rules, modelForm, modelRules } = toRefs(data);
  // æŸ¥è¯¢äº§å“æ ‘
  const getProductTreeList = () => {
    treeLoad.value = true;
    productTreeList()
      .then(res => {
        list.value = res || [];
        normalizeExpandedKeys(list.value);
        expandedKeys.value = Array.from(expandedKeySet);
        treeKey.value += 1;
        nextTick(() => {
          tree.value?.setDefaultExpandedKeys?.(expandedKeys.value);
        });
      })
      .catch(err => {
        console.error(err);
      })
      .finally(() => {
        treeLoad.value = false;
      });
  };
  const handleNodeExpand = data => {
    nextTick(syncExpandedKeysFromTree);
  };
  const handleNodeCollapse = (data, node) => {
    node?.eachNode?.(item => {
      item.collapse();
    });
    nextTick(syncExpandedKeysFromTree);
  };
  // è¿‡æ»¤äº§å“æ ‘
  const searchFilter = () => {
    proxy.$refs.tree.filter(search.value);
  };
  const isTopLevelNode = (data, node) => {
    if (node?.level !== undefined) {
      return node.level === 1;
    }
    return [null, undefined, "", 0, "0"].includes(data?.parentId);
  };
  // æ‰“开产品弹框
  const openProDia = (type, data, node) => {
    if (data && type === "edit" && isTopLevelNode(data, node)) {
      proxy.$modal.msgWarning("一级节点不能编辑或删除");
      return;
    }
    operationType.value = type;
    productDialogTarget.value = null;
    if (type === "add" && data) {
      productDialogTarget.value = { parentId: data.id };
    } else if (type === "edit" && data) {
      let parentId = data.parentId;
      if (
        [null, undefined, ""].includes(parentId) &&
        node?.parent?.data?.id != null
      ) {
        parentId = node.parent.data.id;
      }
      productDialogTarget.value = { id: data.id, parentId };
    }
    productDia.value = true;
    form.value.productName =
      type === "edit" && data ? data.productName : "";
  };
  // æ‰“开规格型号弹框
  const openModelDia = (type, data) => {
    modelOperationType.value = type;
    modelDia.value = true;
    modelForm.value.model = "";
    modelForm.value.unit = "";
    modelForm.value.productCode = "";
    modelForm.value.id = "";
    if (type === "edit") {
      modelForm.value = { ...data };
    }
  };
  // æäº¤äº§å“åç§°ä¿®æ”¹
  const submitForm = () => {
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        if (operationType.value === "add") {
          form.value.parentId =
            productDialogTarget.value?.parentId ?? currentId.value;
          form.value.id = "";
        } else if (operationType.value === "addOne") {
          form.value.id = "";
          form.value.parentId = "";
        } else {
          form.value.id =
            productDialogTarget.value?.id ?? currentId.value;
          form.value.parentId = productDialogTarget.value?.parentId ?? "";
        }
        addOrEditProduct(form.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeProDia();
          getProductTreeList();
        })
        .finally(() => {
          tableLoading.value = false;
        });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
      }
    });
};
// é€‰æ‹©äº§å“
const handleNodeClick = (val, node, el) => {
  // åˆ¤æ–­æ˜¯å¦ä¸ºå¶å­èŠ‚ç‚¹
  isShowButton.value = !(val.children && val.children.length > 0);
  // åªæœ‰å¶å­èŠ‚ç‚¹æ‰æ‰§è¡Œä»¥ä¸‹é€»è¾‘
  currentId.value = val.id;
  currentParentId.value = val.parentId;
  getModelList();
};
  };
  // å…³é—­äº§å“å¼¹æ¡†
  const closeProDia = () => {
    proxy.$refs.formRef.resetFields();
    productDialogTarget.value = null;
    productDia.value = false;
  };
// æäº¤å°ºå¯¸ä¿®æ”¹
const submitModelForm = () => {
  proxy.$refs.modelFormRef.validate((valid) => {
    if (valid) {
      modelForm.value.productId = currentId.value;
      addOrEditProductModel(modelForm.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeModelDia();
        getModelList();
      });
  // åˆ é™¤äº§å“
  const remove = (node, data) => {
    if (isTopLevelNode(data, node)) {
      proxy.$modal.msgWarning("一级节点不能编辑或删除");
      return;
    }
  });
};
// å…³é—­åž‹å·å¼¹æ¡†
const closeModelDia = () => {
  proxy.$refs.modelFormRef.resetFields();
  modelDia.value = false;
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
// æŸ¥è¯¢å°ºå¯¸
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getModelList();
};
const getModelList = () => {
  tableLoading.value = true;
  modelListPage({
    id: currentId.value,
    current: page.current,
    size: page.size,
  }).then((res) => {
    console.log("res", res);
    tableData.value = res.records;
    page.total = res.total;
    tableLoading.value = false;
  });
};
// åˆ é™¤å°ºå¯¸
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      tableLoading.value = true;
      delProductModel(ids)
        .then((res) => {
          proxy.$modal.msgSuccess("删除成功");
          getModelList();
        })
        .finally(() => {
          tableLoading.value = false;
        });
    let ids = [];
    ids.push(data.id);
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
      .then(() => {
        tableLoading.value = true;
        delProduct(ids)
          .then(res => {
            proxy.$modal.msgSuccess("删除成功");
            getProductTreeList();
          })
          .finally(() => {
            tableLoading.value = false;
          });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // é€‰æ‹©äº§å“
  const handleNodeClick = (val, node, el) => {
    // åˆ¤æ–­æ˜¯å¦ä¸ºå¶å­èŠ‚ç‚¹
    isShowButton.value = !(val.children && val.children.length > 0);
    // åªæœ‰å¶å­èŠ‚ç‚¹æ‰æ‰§è¡Œä»¥ä¸‹é€»è¾‘
    currentId.value = val.id;
    currentParentId.value = val.parentId;
    getModelList();
  };
  // æäº¤å°ºå¯¸ä¿®æ”¹
  const submitModelForm = () => {
    proxy.$refs.modelFormRef.validate(valid => {
      if (valid) {
        modelForm.value.productId = currentId.value;
        addOrEditProductModel(modelForm.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeModelDia();
          getModelList();
        });
      }
    });
};
// è°ƒç”¨tree过滤方法 ä¸­æ–‡è‹±è¿‡æ»¤
const filterNode = (value, data, node) => {
  if (!value) {
    //如果数据为空,则返回true,显示所有的数据项
    return true;
  }
  // æŸ¥è¯¢åˆ—表是否有匹配数据,将值小写,匹配英文数据
  let val = value.toLowerCase();
  return chooseNode(val, data, node); // è°ƒç”¨è¿‡æ»¤äºŒå±‚方法
};
// è¿‡æ»¤çˆ¶èŠ‚ç‚¹ / å­èŠ‚ç‚¹ (如果输入的参数是父节点且能匹配,则返回该节点以及其下的所有子节点;如果参数是子节点,则返回该节点的父节点。name是中文字符,enName是英文字符.
const chooseNode = (value, data, node) => {
  if (data.label.indexOf(value) !== -1) {
    return true;
  }
  const level = node.level;
  // å¦‚果传入的节点本身就是一级节点就不用校验了
  if (level === 1) {
    return false;
  }
  // å…ˆå–当前节点的父节点
  let parentData = node.parent;
  // éåŽ†å½“å‰èŠ‚ç‚¹çš„çˆ¶èŠ‚ç‚¹
  let index = 0;
  while (index < level - 1) {
    // å¦‚果匹配到直接返回,此处name值是中文字符,enName是英文字符。判断匹配中英文过滤
    if (parentData.data.label.indexOf(value) !== -1) {
  };
  // å…³é—­åž‹å·å¼¹æ¡†
  const closeModelDia = () => {
    proxy.$refs.modelFormRef.resetFields();
    modelDia.value = false;
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // æŸ¥è¯¢å°ºå¯¸
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getModelList();
  };
  const getModelList = () => {
    tableLoading.value = true;
    modelListPage({
      id: currentId.value,
      current: page.current,
      size: page.size,
    }).then(res => {
      console.log("res", res);
      tableData.value = res.records;
      page.total = res.total;
      tableLoading.value = false;
    });
  };
  // åˆ é™¤è§„格型号
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        tableLoading.value = true;
        delProductModel(ids)
          .then(res => {
            proxy.$modal.msgSuccess("删除成功");
            getModelList();
          })
          .finally(() => {
            tableLoading.value = false;
          });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // è°ƒç”¨tree过滤方法 ä¸­æ–‡è‹±è¿‡æ»¤
  const filterNode = (value, data, node) => {
    if (!value) {
      //如果数据为空,则返回true,显示所有的数据项
      return true;
    }
    // å¦åˆ™çš„话再往上一层做匹配
    parentData = parentData.parent;
    index++;
  }
  // æ²¡åŒ¹é…åˆ°è¿”回false
  return false;
};
getProductTreeList();
    // æŸ¥è¯¢åˆ—表是否有匹配数据,将值小写,匹配英文数据
    let val = value.toLowerCase();
    return chooseNode(val, data, node); // è°ƒç”¨è¿‡æ»¤äºŒå±‚方法
  };
  // è¿‡æ»¤çˆ¶èŠ‚ç‚¹ / å­èŠ‚ç‚¹ (如果输入的参数是父节点且能匹配,则返回该节点以及其下的所有子节点;如果参数是子节点,则返回该节点的父节点。name是中文字符,enName是英文字符.
  const chooseNode = (value, data, node) => {
    if (data.label.indexOf(value) !== -1) {
      return true;
    }
    const level = node.level;
    // å¦‚果传入的节点本身就是一级节点就不用校验了
    if (level === 1) {
      return false;
    }
    // å…ˆå–当前节点的父节点
    let parentData = node.parent;
    // éåŽ†å½“å‰èŠ‚ç‚¹çš„çˆ¶èŠ‚ç‚¹
    let index = 0;
    while (index < level - 1) {
      // å¦‚果匹配到直接返回,此处name值是中文字符,enName是英文字符。判断匹配中英文过滤
      if (parentData.data.label.indexOf(value) !== -1) {
        return true;
      }
      // å¦åˆ™çš„话再往上一层做匹配
      parentData = parentData.parent;
      index++;
    }
    // æ²¡åŒ¹é…åˆ°è¿”回false
    return false;
  };
  getProductTreeList();
</script>
<style scoped>
.product-view {
  display: flex;
}
.left {
  width: 450px;
  min-width: 450px;
  padding: 16px;
  background: #ffffff;
}
.right {
  flex: 1;
  min-width: 0;
  padding: 16px;
  margin-left: 20px;
  background: #ffffff;
}
.custom-tree-node {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  padding-right: 8px;
}
.tree-node-content {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  height: 100%;
  overflow: hidden;
}
.tree-node-content .orange-icon {
  flex-shrink: 0;
}
.tree-node-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.orange-icon {
  color: orange;
  font-size: 18px;
  margin-right: 8px; /* å›¾æ ‡ä¸Žæ–‡å­—之间加点间距 */
}
.product-tree-scroll {
  scrollbar-width: thin;
  scrollbar-color: #c0c4cc #f5f7fa;
}
.product-tree-scroll::-webkit-scrollbar {
  width: 8px;
}
.product-tree-scroll::-webkit-scrollbar-track {
  background: #f5f7fa;
  border-radius: 4px;
}
.product-tree-scroll::-webkit-scrollbar-thumb {
  background: #c0c4cc;
  border-radius: 4px;
}
.product-tree-scroll::-webkit-scrollbar-thumb:hover {
  background: #909399;
}
  .product-view {
    display: flex;
  }
  .left {
    width: 450px;
    min-width: 450px;
    padding: 16px;
    background: #ffffff;
  }
  .right {
    flex: 1;
    min-width: 0;
    padding: 16px;
    margin-left: 20px;
    background: #ffffff;
  }
  .custom-tree-node {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 14px;
    padding-right: 8px;
  }
  .tree-node-content {
    flex: 1;
    min-width: 0;
    display: flex;
    align-items: center;
    height: 100%;
    overflow: hidden;
  }
  .tree-node-content .orange-icon {
    flex-shrink: 0;
  }
  .tree-node-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .orange-icon {
    color: orange;
    font-size: 18px;
    margin-right: 8px; /* å›¾æ ‡ä¸Žæ–‡å­—之间加点间距 */
  }
  .product-tree-scroll {
    scrollbar-width: thin;
    scrollbar-color: #c0c4cc #f5f7fa;
  }
  .product-tree-scroll::-webkit-scrollbar {
    width: 8px;
  }
  .product-tree-scroll::-webkit-scrollbar-track {
    background: #f5f7fa;
    border-radius: 4px;
  }
  .product-tree-scroll::-webkit-scrollbar-thumb {
    background: #c0c4cc;
    border-radius: 4px;
  }
  .product-tree-scroll::-webkit-scrollbar-thumb:hover {
    background: #909399;
  }
</style>
src/views/basicData/supplierManage/components/HomeTab.vue
@@ -1,7 +1,7 @@
<template>
  <div class="app-container">
  <div>
    <div class="search_form">
      <div>
      <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/basicData/supplierManage/filesDia.vue
@@ -164,7 +164,7 @@
}
// ä¸‹è½½é™„ä»¶
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
    proxy.$download.byUrl(row.url, row.originalFilename);
}
// åˆ é™¤
const handleDelete = () => {
src/views/collaborativeApproval/approvalManagement/index.vue
@@ -40,7 +40,7 @@
              </el-tag>
            </div>
          </div>
          <div class="header-actions">
          <div class="header-actions" v-if="approverList.length > 0">
            <el-button @click="handleReset" size="default">
              <el-icon><RefreshLeft /></el-icon>
              é‡ç½®
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -1,85 +1,103 @@
<template>
  <div>
    <el-dialog
      v-model="dialogFormVisible"
      :title="operationType === 'approval' ? '审批' : '详情'"
      width="700px"
      @close="closeDia"
    >
            <el-form :model="form" label-width="140px" label-position="top" ref="formRef">
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="流程编号:" prop="approveId">
                            <el-input v-model="form.approveId" placeholder="自动编号" clearable disabled/>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="申请部门:">
                            <el-select
                                disabled
                                v-model="form.approveDeptId"
                                placeholder="选择部门"
                            >
                                <el-option
                                    v-for="user in productOptions"
                                    :key="user.deptId"
                                    :label="user.deptName"
                                    :value="user.deptId"
                                />
                            </el-select>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row v-if="!isQuotationApproval && !isPurchaseApproval">
                    <el-col :span="24">
                        <el-form-item :label="props.approveType == 5 ? '采购合同号:' : '审批事由:'" prop="approveReason">
                            <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" disabled/>
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'approval' ? '审批' : '详情'"
               width="700px"
               @close="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               ref="formRef">
        <el-row>
          <el-col :span="24">
            <el-form-item label="流程编号:"
                          prop="approveId">
              <el-input v-model="form.approveId"
                        placeholder="自动编号"
                        clearable
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="申请部门:">
              <el-select disabled
                         v-model="form.approveDeptId"
                         placeholder="选择部门">
                <el-option v-for="user in productOptions"
                           :key="user.deptId"
                           :label="user.deptName"
                           :value="user.deptId" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="!isQuotationApproval && !isPurchaseApproval">
          <el-col :span="24">
            <el-form-item :label="props.approveType == 5 ? '采购合同号:' : '审批事由:'"
                          prop="approveReason">
              <el-input v-model="form.approveReason"
                        placeholder="请输入"
                        clearable
                        type="textarea"
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <!-- æŠ¥ä»·å®¡æ‰¹ï¼šå±•示报价详情(复用销售报价"查看详情对话框"内容结构) -->
      <div v-if="isQuotationApproval" style="margin: 10px 0 18px;">
      <div v-if="isQuotationApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">报价详情</el-divider>
        <el-skeleton :loading="quotationLoading" animated>
        <el-skeleton :loading="quotationLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" description="未查询到对应报价详情" />
            <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo"
                      description="未查询到对应报价详情" />
            <template v-else>
              <el-descriptions :column="2" border>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
                <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
                <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
                <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
                <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item>
                <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
                <el-descriptions-item label="报价总额" :span="2">
                <el-descriptions-item label="报价总额"
                                      :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
                  </span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="currentQuotation.products || []" border style="width: 100%">
                  <el-table-column prop="product" label="产品名称" />
                  <el-table-column prop="specification" label="尺寸" />
                  <el-table-column prop="unit" label="单位" />
                  <el-table-column prop="unitPrice" label="单价">
                <el-table :data="currentQuotation.products || []"
                          border
                          style="width: 100%">
                  <el-table-column prop="product"
                                   label="产品名称" />
                  <el-table-column prop="specification"
                                   label="规格型号" />
                  <el-table-column prop="unit"
                                   label="单位" />
                  <el-table-column prop="unitPrice"
                                   label="单价">
                    <template #default="scope">Â¥{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                </el-table>
              </div>
              <div v-if="currentQuotation.remark" style="margin-top: 20px;">
              <div v-if="currentQuotation.remark"
                   style="margin-top: 20px;">
                <h4>备注</h4>
                <p>{{ currentQuotation.remark }}</p>
              </div>
@@ -87,20 +105,26 @@
          </template>
        </el-skeleton>
      </div>
      <!-- é‡‡è´­å®¡æ‰¹ï¼šå±•示采购详情 -->
      <div v-if="isPurchaseApproval" style="margin: 10px 0 18px;">
      <div v-if="isPurchaseApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">采购详情</el-divider>
        <el-skeleton :loading="purchaseLoading" animated>
        <el-skeleton :loading="purchaseLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber" description="未查询到对应采购详情" />
            <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
                      description="未查询到对应采购详情" />
            <template v-else>
              <el-descriptions :column="2" border>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="采购合同号">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item>
                <el-descriptions-item label="供应商名称">{{ currentPurchase.supplierName }}</el-descriptions-item>
                <el-descriptions-item label="项目名称">{{ currentPurchase.projectName }}</el-descriptions-item>
@@ -108,24 +132,32 @@
                <el-descriptions-item label="签订日期">{{ currentPurchase.executionDate }}</el-descriptions-item>
                <el-descriptions-item label="录入日期">{{ currentPurchase.entryDate }}</el-descriptions-item>
                <el-descriptions-item label="付款方式">{{ currentPurchase.paymentMethod }}</el-descriptions-item>
                <el-descriptions-item label="合同金额" :span="2">
                <el-descriptions-item label="合同金额"
                                      :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    Â¥{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
                  </span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="currentPurchase.productData || []" border style="width: 100%">
                  <el-table-column prop="productCategory" label="产品名称" />
                  <el-table-column prop="specificationModel" label="尺寸" />
                  <el-table-column prop="unit" label="单位" />
                  <el-table-column prop="quantity" label="数量" />
                  <el-table-column prop="taxInclusiveUnitPrice" label="含税单价">
                <el-table :data="currentPurchase.productData || []"
                          border
                          style="width: 100%">
                  <el-table-column prop="productCategory"
                                   label="产品名称" />
                  <el-table-column prop="specificationModel"
                                   label="规格型号" />
                  <el-table-column prop="unit"
                                   label="单位" />
                  <el-table-column prop="quantity"
                                   label="数量" />
                  <el-table-column prop="taxInclusiveUnitPrice"
                                   label="含税单价">
                    <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                  <el-table-column prop="taxInclusiveTotalPrice" label="含税总价">
                  <el-table-column prop="taxInclusiveTotalPrice"
                                   label="含税总价">
                    <template #default="scope">Â¥{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                </el-table>
@@ -134,52 +166,138 @@
          </template>
        </el-skeleton>
      </div>
      <el-form :model="{ activities }" ref="formRef" label-position="top">
        <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
          <el-step
            v-for="(activity, index) in activities"
            :key="index"
                        finish-status="success"
            :title="getNodeTitle(index, activities.length)"
            :description="activity.approveNodeUser"
            :icon="getNodeIcon(activity, index)"
          >
                        <template #icon>
                            <el-icon v-if="activity.approveNodeStatus === 2" color="red" :size="22"><WarningFilled /></el-icon>
                            <el-icon v-else-if="activity.isShen" color="#1890ff" :size="22"><Edit /></el-icon>
                            <el-icon v-else-if="activity.approveNodeStatus === 1" color="#67C23A" :size="26"><Check /></el-icon>
                            <el-icon v-else color="#C0C4CC" :size="22"><MoreFilled /></el-icon>
                        </template>
      <!-- å‘货审批:展示发货详情 -->
      <div v-if="isDeliveryApproval"
           style="margin: 10px 0 18px;">
        <el-divider content-position="left">发货详情</el-divider>
        <el-skeleton :loading="deliveryLoading"
                     animated>
          <template #template>
            <el-skeleton-item variant="h3"
                              style="width: 30%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
            <el-skeleton-item variant="text"
                              style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentDelivery || !currentDelivery.shippingInfo"
                      description="未查询到对应发货详情" />
            <template v-else>
              <el-descriptions :column="2"
                               border>
                <el-descriptions-item label="销售订单">{{ currentDelivery.shippingInfo.salesContractNo || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货订单号">{{ currentDelivery.shippingInfo.shippingNo || '--' }}</el-descriptions-item>
                <el-descriptions-item label="客户名称">{{ currentDelivery.shippingInfo.customerName || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货类型">{{ currentDelivery.shippingInfo.type || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货日期">{{ currentDelivery.shippingInfo.shippingDate || '--' }}</el-descriptions-item>
                <el-descriptions-item label="审核状态">{{ currentDelivery.shippingInfo.status || '--' }}</el-descriptions-item>
                <el-descriptions-item label="发货车牌号">{{ currentDelivery.shippingInfo.shippingCarNumber || '--' }}</el-descriptions-item>
                <el-descriptions-item label="快递公司">{{ currentDelivery.shippingInfo.expressCompany || '--' }}</el-descriptions-item>
                <el-descriptions-item label="快递单号"
                                      :span="2">{{ currentDelivery.shippingInfo.expressNumber || '--' }}</el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="deliveryProductList"
                          border
                          size="small"
                          style="width: 100%">
                  <el-table-column prop="batchNo"
                                   label="批号"
                                   show-overflow-tooltip />
                  <el-table-column prop="productName"
                                   label="产品名称"
                                   show-overflow-tooltip />
                  <el-table-column prop="specificationModel"
                                   label="规格型号"
                                   show-overflow-tooltip />
                  <el-table-column prop="deliveryQuantity"
                                   label="发货数量"
                                   align="center" />
                </el-table>
              </div>
              <div v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length"
                   style="margin-top: 20px;">
                <h4>发货图片</h4>
                <ImagePreview :file-list="currentDelivery.shippingInfo.storageBlobVOs" />
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <el-form :model="{ activities }"
               ref="formRef"
               label-position="top">
        <el-steps :active="getActiveStep()"
                  finish-status="success"
                  process-status="process"
                  align-center
                  direction="vertical">
          <el-step v-for="(activity, index) in activities"
                   :key="index"
                   finish-status="success"
                   :title="getNodeTitle(index, activities.length)"
                   :description="activity.approveNodeUser"
                   :icon="getNodeIcon(activity, index)">
            <template #icon>
              <el-icon v-if="activity.approveNodeStatus === 2"
                       color="red"
                       :size="22">
                <WarningFilled />
              </el-icon>
              <el-icon v-else-if="activity.isShen"
                       color="#1890ff"
                       :size="22">
                <Edit />
              </el-icon>
              <el-icon v-else-if="activity.approveNodeStatus === 1"
                       color="#67C23A"
                       :size="26">
                <Check />
              </el-icon>
              <el-icon v-else
                       color="#C0C4CC"
                       :size="22">
                <MoreFilled />
              </el-icon>
            </template>
            <template #title>
              <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
            </template>
            <template #description>
              <div class="node-user">
                <div class="avatar-wrapper">
                  <img :src="userStore.avatar" class="user-avatar" alt=""/>
                  <img :src="userStore.avatar"
                       class="user-avatar"
                       alt="" />
                </div>
                <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span>
              </div>
              <div v-if="!activity.isShen" class="node-reason">
              <div v-if="!activity.isShen"
                   class="node-reason">
                <span>审批意见:</span>{{ activity.approveNodeReason }}
              </div>
              <div v-else-if="activity.isShen">
                <el-form-item
                  :prop="'activities.' + index + '.approveNodeReason'"
                  :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]"
                >
                  <el-input v-model="activity.approveNodeReason" clearable type="textarea" :disabled="operationType === 'view'"></el-input>
                <el-form-item :prop="'activities.' + index + '.approveNodeReason'"
                              :rules="[{ required: true, message: '审批意见不能为空', trigger: 'blur' }]">
                  <el-input v-model="activity.approveNodeReason"
                            clearable
                            type="textarea"
                            :disabled="operationType === 'view'"></el-input>
                </el-form-item>
              </div>
            </template>
          </el-step>
        </el-steps>
      </el-form>
      <template #footer v-if="operationType === 'approval'">
      <template #footer
                v-if="operationType === 'approval'">
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm(2)">不通过</el-button>
          <el-button type="primary" @click="submitForm(1)">通过</el-button>
          <el-button type="primary"
                     @click="submitForm(2)">不通过</el-button>
          <el-button type="primary"
                     @click="submitForm(1)">通过</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
@@ -188,221 +306,329 @@
</template>
<script setup>
import { computed, getCurrentInstance, nextTick, reactive, ref, toRefs } from "vue";
import {
    approveProcessDetails,
    getDept,
    updateApproveNode
} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user.js";
import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue'
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
const emit = defineEmits(['close'])
const { proxy } = getCurrentInstance()
  import {
    computed,
    getCurrentInstance,
    nextTick,
    reactive,
    ref,
    toRefs,
  } from "vue";
  import {
    approveProcessDetails,
    getDept,
    updateApproveNode,
  } from "@/api/collaborativeApproval/approvalProcess.js";
  import useUserStore from "@/store/modules/user.js";
  import {
    WarningFilled,
    Edit,
    Check,
    MoreFilled,
  } from "@element-plus/icons-vue";
  import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
  import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
  import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js";
  import ImagePreview from "@/components/AttachmentPreview/image/index.vue";
  const emit = defineEmits(["close"]);
  const { proxy } = getCurrentInstance();
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
})
const dialogFormVisible = ref(false);
const operationType = ref('')
const activities = ref([])
const formRef = ref(null);
const userStore = useUserStore()
const productOptions = ref([]);
const quotationLoading = ref(false)
const currentQuotation = ref({})
const purchaseLoading = ref(false)
const currentPurchase = ref({})
const isQuotationApproval = computed(() => Number(props.approveType) === 6)
const isPurchaseApproval = computed(() => Number(props.approveType) === 5)
const data = reactive({
    form: {
        approveId: "",
        approveDeptId: "",
        approveReason: "",
        checkResult: "",
    },
});
const { form } = toRefs(data);
// èŠ‚ç‚¹æ ‡é¢˜
const getNodeTitle = (index, len) => {
  if (index === len - 1) return '结束';
  return '审批';
};
// èŽ·å–å½“å‰æ¿€æ´»æ­¥éª¤
const getActiveStep = () => {
  // å¦‚果所有 isShen éƒ½ä¸º false,返回最后一个步骤(全部完成)
  const hasActive = activities.value.some(a => a.isShen === true);
  if (!hasActive) return activities.value.length;
  // å½“前节点索引
  return activities.value.findIndex(a => a.isShen  == true);
};
// æ­¥éª¤icon
const getNodeIcon = (activity, index) => {
  if (activity.approveNodeStatus === 2) return 'el-icon-warning'; // ä¸é€šè¿‡
  if (activity.isShen) return 'Edit';
  return '';
};
// æ‰“开弹框
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  currentQuotation.value = {}
  currentPurchase.value = {}
    form.value = {...row}
    // ç«‹å³æ¸…除表单验证状态(因为字段是disabled的,不需要验证)
    nextTick(() => {
        if (formRef.value) {
            formRef.value.clearValidate();
        }
    });
    // ç¡®ä¿é€‰é¡¹åŠ è½½å®ŒæˆåŽå†åŒ¹é…å€¼ç±»åž‹
    getProductOptions().then(() => {
        // ç¡®ä¿å€¼ç±»åž‹åŒ¹é…ï¼ˆå¦‚果选项已加载)
        if (productOptions.value.length > 0 && form.value.approveDeptId) {
            const matchedOption = productOptions.value.find(opt =>
                opt.deptId == form.value.approveDeptId ||
                String(opt.deptId) === String(form.value.approveDeptId)
            );
            if (matchedOption) {
                form.value.approveDeptId = matchedOption.deptId;
            }
        }
        // å†æ¬¡æ¸…除验证,确保选项加载后值匹配正确
        nextTick(() => {
            if (formRef.value) {
                formRef.value.clearValidate();
            }
        });
    });
  // æŠ¥ä»·å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"报价单号"去查报价列表
  if (isQuotationApproval.value) {
    const quotationNo = row?.approveReason;
    if (quotationNo) {
      quotationLoading.value = true
      getQuotationList({ quotationNo }).then((res) => {
        const records = res?.data?.records || []
        currentQuotation.value = records[0] || {}
      }).finally(() => {
        quotationLoading.value = false
      })
    }
  }
  // é‡‡è´­å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"采购合同号"去查采购详情
  if (isPurchaseApproval.value) {
    const purchaseContractNumber = row?.approveReason;
    if (purchaseContractNumber) {
      purchaseLoading.value = true
      getPurchaseByCode({ purchaseContractNumber }).then((res) => {
        currentPurchase.value = res
      }).catch((err) => {
        console.error('查询采购详情失败:', err)
        proxy.$modal.msgError('查询采购详情失败')
      }).finally(() => {
        purchaseLoading.value = false
      })
    }
  }
  approveProcessDetails(row.approveId).then((res) => {
    activities.value = res.data
    // å¢žåŠ isApproval字段
    activities.value.forEach(item => {
            if (item.url && item.url.includes('word')) {
                item.urlTem = item.url.replaceAll('word', 'img')
            } else {
                item.urlTem = item.url
            }
      if (item.approveNodeStatus === 2) {
        item.isApproval = '已驳回';
      } else if (item.approveNodeStatus === 1) {
        item.isApproval = '已同意';
      } else {
        item.isApproval = '未审批';
      }
    })
  })
}
const getProductOptions = () => {
    return getDept().then((res) => {
        productOptions.value = res.data;
    });
};
// æäº¤å®¡æ‰¹
const submitForm = (status) => {
  const filteredActivities = activities.value.filter(activity => activity.isShen);
  if (!filteredActivities || filteredActivities.length === 0) {
    proxy.$modal.msgError("未找到待审批的节点");
    return;
  }
  const currentActivity = filteredActivities[0];
  if (!currentActivity) {
    proxy.$modal.msgError("未找到待审批的节点");
    return;
  }
  currentActivity.approveNodeStatus = status;
  // åˆ¤æ–­æ˜¯å¦ä¸ºæœ€åŽä¸€æ­¥
  const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length-1;
  updateApproveNode({ ...currentActivity, isLast }).then(() => {
    proxy.$modal.msgSuccess("提交成功");
    closeDia();
  const props = defineProps({
    approveType: {
      type: [Number, String],
      default: 0,
    },
  });
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  quotationLoading.value = false
  currentQuotation.value = {}
  purchaseLoading.value = false
  currentPurchase.value = {}
  emit('close')
};
defineExpose({
  openDialog,
});
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const activities = ref([]);
  const formRef = ref(null);
  const userStore = useUserStore();
  const productOptions = ref([]);
  const quotationLoading = ref(false);
  const currentQuotation = ref({});
  const purchaseLoading = ref(false);
  const currentPurchase = ref({});
  const deliveryLoading = ref(false);
  const currentDelivery = ref({});
  const deliveryProductList = ref([]);
  const isQuotationApproval = computed(() => Number(props.approveType) === 6);
  const isPurchaseApproval = computed(() => Number(props.approveType) === 5);
  const isDeliveryApproval = computed(() => Number(props.approveType) === 7);
  const data = reactive({
    form: {
      approveId: "",
      approveDeptId: "",
      approveReason: "",
      checkResult: "",
    },
  });
  const { form } = toRefs(data);
  // èŠ‚ç‚¹æ ‡é¢˜
  const getNodeTitle = (index, len) => {
    if (index === len - 1) return "结束";
    return "审批";
  };
  // èŽ·å–å½“å‰æ¿€æ´»æ­¥éª¤
  const getActiveStep = () => {
    // å¦‚果所有 isShen éƒ½ä¸º false,返回最后一个步骤(全部完成)
    const hasActive = activities.value.some(a => a.isShen === true);
    if (!hasActive) return activities.value.length;
    // å½“前节点索引
    return activities.value.findIndex(a => a.isShen == true);
  };
  // æ­¥éª¤icon
  const getNodeIcon = (activity, index) => {
    if (activity.approveNodeStatus === 2) return "el-icon-warning"; // ä¸é€šè¿‡
    if (activity.isShen) return "Edit";
    return "";
  };
  // æ‰“开弹框
  const openDialog = (type, row) => {
    operationType.value = type;
    dialogFormVisible.value = true;
    currentQuotation.value = {};
    currentPurchase.value = {};
    form.value = { ...row };
    // ç«‹å³æ¸…除表单验证状态(因为字段是disabled的,不需要验证)
    nextTick(() => {
      if (formRef.value) {
        formRef.value.clearValidate();
      }
    });
    // ç¡®ä¿é€‰é¡¹åŠ è½½å®ŒæˆåŽå†åŒ¹é…å€¼ç±»åž‹
    getProductOptions().then(() => {
      // ç¡®ä¿å€¼ç±»åž‹åŒ¹é…ï¼ˆå¦‚果选项已加载)
      if (productOptions.value.length > 0 && form.value.approveDeptId) {
        const matchedOption = productOptions.value.find(
          opt =>
            opt.deptId == form.value.approveDeptId ||
            String(opt.deptId) === String(form.value.approveDeptId)
        );
        if (matchedOption) {
          form.value.approveDeptId = matchedOption.deptId;
        }
      }
      // å†æ¬¡æ¸…除验证,确保选项加载后值匹配正确
      nextTick(() => {
        if (formRef.value) {
          formRef.value.clearValidate();
        }
      });
    });
    // æŠ¥ä»·å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"报价单号"去查报价列表
    if (isQuotationApproval.value) {
      const quotationNo = row?.approveReason;
      if (quotationNo) {
        quotationLoading.value = true;
        getQuotationList({ quotationNo })
          .then(res => {
            const records = res?.data?.records || [];
            currentQuotation.value = records[0] || {};
          })
          .finally(() => {
            quotationLoading.value = false;
          });
      }
    }
    // é‡‡è´­å®¡æ‰¹ï¼šç”¨å®¡æ‰¹äº‹ç”±å­—段承载的"采购合同号"去查采购详情
    if (isPurchaseApproval.value) {
      const purchaseContractNumber = row?.approveReason;
      if (purchaseContractNumber) {
        purchaseLoading.value = true;
        getPurchaseByCode({ purchaseContractNumber })
          .then(res => {
            currentPurchase.value = res;
          })
          .catch(err => {
            console.error("查询采购详情失败:", err);
            proxy.$modal.msgError("查询采购详情失败");
          })
          .finally(() => {
            purchaseLoading.value = false;
          });
      }
    }
    // å‘货审批:用审批事由字段承载的"发货单号"去查发货详情
    if (isDeliveryApproval.value) {
      const deliveryNo = row?.approveReason;
      if (deliveryNo) {
        deliveryLoading.value = true;
        currentDelivery.value = {};
        deliveryProductList.value = [];
        getDeliveryDetailByShippingNo({ shippingNo: deliveryNo })
          .then(res => {
            const detailData = res?.data || res || {};
            currentDelivery.value = detailData;
            deliveryProductList.value =
              detailData.shippingProductDetailDtoList || [];
          })
          .catch(err => {
            console.error("查询发货详情失败:", err);
            proxy.$modal.msgError("查询发货详情失败");
          })
          .finally(() => {
            deliveryLoading.value = false;
          });
      }
    }
    approveProcessDetails(row.approveId).then(res => {
      activities.value = res.data;
      // å¢žåŠ isApproval字段
      activities.value.forEach(item => {
        if (item.url && item.url.includes("word")) {
          item.urlTem = item.url.replaceAll("word", "img");
        } else {
          item.urlTem = item.url;
        }
        if (item.approveNodeStatus === 2) {
          item.isApproval = "已驳回";
        } else if (item.approveNodeStatus === 1) {
          item.isApproval = "已同意";
        } else {
          item.isApproval = "未审批";
        }
      });
    });
  };
  const getDeliveryProductInfoList = () => {
    const row = currentDelivery.value;
    if (!row) return [];
    const normalizeBatchNoList = value => {
      if (Array.isArray(value)) return value;
      if (typeof value === "string" && value.includes(",")) {
        return value
          .split(",")
          .map(item => item.trim())
          .filter(Boolean);
      }
      return value ? [value] : [];
    };
    const detailList = deliveryProductList.value.length
      ? deliveryProductList.value
      : [
          row.batchNoDetailList,
          row.batchNoList,
          row.shippingBatchList,
          row.shippingInfoDetailList,
          row.detailList,
          row.batchDetailList,
        ].find(value => Array.isArray(value) && value.length);
    const batchNoList = normalizeBatchNoList(row.batchNo);
    const toTableRow = (item = {}) => ({
      batchNo:
        typeof item === "string" || typeof item === "number"
          ? item
          : item.batchNo ?? item.batchNumber ?? row.batchNo ?? "--",
      productName: item.productName ?? row.productName ?? "--",
      specificationModel:
        item.specificationModel ?? item.model ?? row.specificationModel ?? "--",
      deliveryQuantity:
        item.deliveryQuantity ??
        item.quantity ??
        item.shippingQuantity ??
        row.deliveryQuantity ??
        row.quantity ??
        "--",
    });
    if (detailList?.length) {
      return detailList.map(toTableRow);
    }
    if (batchNoList.length) {
      return batchNoList.map(batchNo => toTableRow({ batchNo }));
    }
    return [toTableRow()];
  };
  const getApprovalStatusText = status => {
    const statusMap = {
      0: "待审核",
      1: "审核通过",
      2: "审核拒绝",
      3: "审核中",
    };
    return statusMap[status] || "待审核";
  };
  const getProductOptions = () => {
    return getDept().then(res => {
      productOptions.value = res.data;
    });
  };
  // æäº¤å®¡æ‰¹
  const submitForm = status => {
    const filteredActivities = activities.value.filter(
      activity => activity.isShen
    );
    if (!filteredActivities || filteredActivities.length === 0) {
      proxy.$modal.msgError("未找到待审批的节点");
      return;
    }
    const currentActivity = filteredActivities[0];
    if (!currentActivity) {
      proxy.$modal.msgError("未找到待审批的节点");
      return;
    }
    currentActivity.approveNodeStatus = status;
    // åˆ¤æ–­æ˜¯å¦ä¸ºæœ€åŽä¸€æ­¥
    const isLast =
      activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
    updateApproveNode({ ...currentActivity, isLast }).then(() => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
    });
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    quotationLoading.value = false;
    currentQuotation.value = {};
    purchaseLoading.value = false;
    currentPurchase.value = {};
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped>
.node-user {
  margin: 10px 0;
  font-size: 16px;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 8px;
}
.node-status {
  color: #1890ff;
  margin-left: 8px;
  font-size: 14px;
}
.node-reason {
  font-size: 15px;
  color: #333;
  margin: 10px 0;
}
.user-avatar {
    cursor: pointer;
    width: 30px;
    height: 30px;
    border-radius: 50px;
}
.signImg {
    cursor: pointer;
    width: 200px;
    height: 60px;
}
  .node-user {
    margin: 10px 0;
    font-size: 16px;
    font-weight: 600;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .node-status {
    color: #1890ff;
    margin-left: 8px;
    font-size: 14px;
  }
  .node-reason {
    font-size: 15px;
    color: #333;
    margin: 10px 0;
  }
  .user-avatar {
    cursor: pointer;
    width: 30px;
    height: 30px;
    border-radius: 50px;
  }
  .signImg {
    cursor: pointer;
    width: 200px;
    height: 60px;
  }
</style>
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -101,17 +101,7 @@
        <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>
@@ -140,6 +130,7 @@
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
import useUserStore from "@/store/modules/user";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
const userStore = useUserStore();
const dialogFormVisible = ref(false);
@@ -158,7 +149,6 @@
    approveDeptName: "",
    approveReason: "",
    checkResult: "",
    tempFileIds: [],
    startDate: "", // è¯·å‡å¼€å§‹æ—¶é—´
    endDate: "", // è¯·å‡ç»“束时间
    price: null, // æŠ¥é”€é‡‘额
@@ -214,6 +204,7 @@
    currentApproveStatus.value = row.approveStatus
    approveProcessGetInfo({id: row.approveId,approveReason: '1'}).then(res => {
      form.value = {...res.data}
      fileList.value = res.data.storageBlobVOS
    })
  }
}
@@ -222,8 +213,8 @@
    productOptions.value = res.data;
    // å¦‚果已有部门ID,自动设置部门名称(用于验证)
    if (form.value.approveDeptId && productOptions.value.length > 0) {
      const matchedDept = productOptions.value.find(dept =>
        dept.deptId == form.value.approveDeptId ||
      const matchedDept = productOptions.value.find(dept =>
        dept.deptId == form.value.approveDeptId ||
        String(dept.deptId) === String(form.value.approveDeptId)
      );
      if (matchedDept) {
@@ -242,7 +233,7 @@
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
@@ -279,6 +270,8 @@
      return
    }
  }
  form.value.storageBlobDTOList = fileList.value
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      if (operationType.value === "add" || currentApproveStatus.value == 3) {
@@ -302,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
@@ -32,8 +32,7 @@
  tableData.value = list
}
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
    proxy.$download.byUrl(row.url, row.originalFilename);
}
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -33,9 +33,9 @@
          </div>
          <div class="filter-item">
            <span class="filter-label">审批状态</span>
            <el-select
              v-model="searchForm.approveStatus"
              clearable
            <el-select
              v-model="searchForm.approveStatus"
              clearable
              @change="handleQuery"
              placeholder="请选择状态"
              class="search-select"
@@ -109,19 +109,18 @@
          </div>
        </div>
      </template>
      <div class="custom-table">
        <PIMTable
          rowKey="id"
          :column="tableColumnCopy"
          :tableData="tableData"
          :page="page"
          :isSelection="true"
          @selection-change="handleSelectionChange"
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
        ></PIMTable>
      </div>
      <PIMTable
        rowKey="id"
        :column="tableColumnCopy"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
        class="custom-table"
      ></PIMTable>
    </el-card>
    <!-- å¼¹çª—组件 -->
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/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
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/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/purchaseApproval/index.vue
@@ -2,678 +2,771 @@
  <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"
                style="width: 240px"
                placeholder="请输入"
                @change="handleQuery"
                clearable
                :prefix-icon="Search"
            />
            <el-input v-model="searchForm.purchaseContractNumber"
                      style="width: 240px"
                      placeholder="请输入"
                      @change="handleQuery"
                      clearable
                      :prefix-icon="Search" />
          </el-form-item>
          <el-form-item label="销售合同号:">
            <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search"
            <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"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys"
        :row-key="(row) => row.id"
        show-summary
        :summary-method="summarizeMainTable"
        @expand-change="expandChange"
        height="calc(100vh - 18.5em)"
        :row-class-name="tableRowClassName"
      >
        <el-table-column align="center" type="selection" width="55" />
      <el-table :data="tableData"
                border
                v-loading="tableLoading"
                @selection-change="handleSelectionChange"
                :expand-row-keys="expandedRowKeys"
                :row-key="(row) => row.id"
                show-summary
                :summary-method="summarizeMainTable"
                @expand-change="expandChange"
                height="calc(100vh - 18.5em)"
                :row-class-name="tableRowClassName">
        <el-table-column align="center"
                         type="selection"
                         width="55" />
        <el-table-column type="expand">
          <template #default="props">
            <el-table
              :data="props.row.children"
              border
              show-summary
              :summary-method="summarizeChildrenTable"
            >
              <el-table-column
                align="center"
                label="序号"
                type="index"
                width="60"
              />
              <el-table-column label="产品大类" prop="productCategory" />
              <el-table-column label="尺寸" prop="specificationModel" />
              <el-table-column label="单位" prop="unit" />
              <el-table-column label="数量" prop="quantity" />
              <el-table-column label="税率(%)" prop="taxRate" />
              <el-table-column
                label="含税单价(元)"
                prop="taxInclusiveUnitPrice"
                :formatter="formattedNumber"
              />
              <el-table-column
                label="含税总价(元)"
                prop="taxInclusiveTotalPrice"
                :formatter="formattedNumber"
              />
              <el-table-column
                label="不含税总价(元)"
                prop="taxExclusiveTotalPrice"
                :formatter="formattedNumber"
              />
            <el-table :data="props.row.children"
                      border
                      show-summary
                      :summary-method="summarizeChildrenTable">
              <el-table-column align="center"
                               label="序号"
                               type="index"
                               width="60" />
              <el-table-column label="产品大类"
                               prop="productCategory" />
              <el-table-column label="尺寸"
                               prop="specificationModel" />
              <el-table-column label="单位"
                               prop="unit" />
              <el-table-column label="数量"
                               prop="quantity" />
              <el-table-column label="税率(%)"
                               prop="taxRate" />
              <el-table-column label="含税单价(元)"
                               prop="taxInclusiveUnitPrice"
                               :formatter="formattedNumber" />
              <el-table-column label="含税总价(元)"
                               prop="taxInclusiveTotalPrice"
                               :formatter="formattedNumber" />
              <el-table-column label="不含税总价(元)"
                               prop="taxExclusiveTotalPrice"
                               :formatter="formattedNumber" />
            </el-table>
          </template>
        </el-table-column>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column
          label="采购合同号"
          prop="purchaseContractNumber"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="销售合同号"
          prop="salesContractNo"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="供应商名称"
          width="240"
          prop="supplierName"
          show-overflow-tooltip
        />
        <el-table-column label="订单状态" width="100" align="center">
        <el-table-column align="center"
                         label="序号"
                         type="index"
                         width="60" />
        <el-table-column label="采购合同号"
                         prop="purchaseContractNumber"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column label="销售合同号"
                         prop="salesContractNo"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column label="供应商名称"
                         width="240"
                         prop="supplierName"
                         show-overflow-tooltip />
        <el-table-column label="订单状态"
                         width="100"
                         align="center">
          <template #default="scope">
            <el-tag v-if="scope.row.isInvalid" type="danger" size="small">失效</el-tag>
            <el-tag v-else type="success" size="small">正常</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="项目名称"
          prop="projectName"
          width="420"
          show-overflow-tooltip
        />
        <el-table-column
            label="审批状态"
            prop="approvalStatus"
            width="200"
            show-overflow-tooltip
        >
        <el-table-column label="项目名称"
                         prop="projectName"
                         width="420"
                         show-overflow-tooltip />
        <el-table-column label="审批状态"
                         prop="approvalStatus"
                         width="200"
                         show-overflow-tooltip>
          <template #default="scope">
            <el-tag
                size="small"
            >
            <el-tag size="small">
              {{ approvalStatusText[scope.row.approvalStatus] || '未知状态' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="付款方式"
          width="100"
          prop="paymentMethod"
          show-overflow-tooltip
        />
        <el-table-column
          label="合同金额(元)"
          prop="contractAmount"
           width="200"
          show-overflow-tooltip
          :formatter="formattedNumber"
        />
        <el-table-column
          label="录入人"
          prop="recorderName"
           width="100"
          show-overflow-tooltip
        />
        <el-table-column
          label="录入日期"
          prop="entryDate"
           width="100"
          show-overflow-tooltip
        />
        <el-table-column
          fixed="right"
          label="操作"
          min-width="150"
          align="center"
        >
        <el-table-column label="付款方式"
                         width="100"
                         prop="paymentMethod"
                         show-overflow-tooltip />
        <el-table-column label="合同金额(元)"
                         prop="contractAmount"
                         width="200"
                         show-overflow-tooltip
                         :formatter="formattedNumber" />
        <el-table-column label="录入人"
                         prop="recorderName"
                         width="100"
                         show-overflow-tooltip />
        <el-table-column label="录入日期"
                         prop="entryDate"
                         width="100"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         min-width="150"
                         align="center">
          <template #default="scope">
            <el-button
              link
              type="primary"
              size="small"
              @click="approvePurchase(scope.row)"
              :disabled="scope.row.approvalStatus !== 0"
              >审批</el-button
            >
            <el-button
                link
                type="primary"
                size="small"
                @click="rejectPurchase(scope.row)"
                :disabled="scope.row.approvalStatus !== 0"
            >拒绝审批</el-button
            >
            <el-button link
                       type="primary"
                       size="small"
                       @click="approvePurchase(scope.row)"
                       :disabled="scope.row.approvalStatus !== 0">审批</el-button>
            <el-button link
                       type="primary"
                       size="small"
                       @click="rejectPurchase(scope.row)"
                       :disabled="scope.row.approvalStatus !== 0">拒绝审批</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
      <pagination v-show="total > 0"
                  :total="total"
                  layout="total, sizes, prev, pager, next, jumper"
                  :page="page.current"
                  :limit="page.size"
                  @pagination="paginationChange" />
    </div>
  </div>
</template>
<script setup>
import { getToken } from "@/utils/auth";
import pagination from "@/components/PIMTable/Pagination.vue";
import { ref, onMounted, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
import { userListNoPage } from "@/api/system/user.js";
import {
  getSalesLedgerWithProducts,
  addOrUpdateSalesLedgerProduct,
  delProduct,
  delLedgerFile,
  getProductInfoByContractNo,
} from "@/api/salesManagement/salesLedger.js";
import {
  addOrEditPurchase,
  delPurchase,
  getSalesNo,
  purchaseListPage,
  productList,
  getPurchaseById,
  getOptions,
  createPurchaseNo, updateApprovalStatus,
} from "@/api/procurementManagement/procurementLedger.js";
import useFormData from "@/hooks/useFormData.js";
import QRCode from "qrcode";
  import { getToken } from "@/utils/auth";
  import pagination from "@/components/PIMTable/Pagination.vue";
  import {
    ref,
    onMounted,
    reactive,
    toRefs,
    getCurrentInstance,
    nextTick,
  } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import { ElMessageBox } from "element-plus";
  import { userListNoPage } from "@/api/system/user.js";
  import {
    getSalesLedgerWithProducts,
    addOrUpdateSalesLedgerProduct,
    delProduct,
    delLedgerFile,
    getProductInfoByContractNo,
  } from "@/api/salesManagement/salesLedger.js";
  import {
    addOrEditPurchase,
    delPurchase,
    getSalesNo,
    purchaseListPage,
    productList,
    getPurchaseById,
    getOptions,
    createPurchaseNo,
    updateApprovalStatus,
  } from "@/api/procurementManagement/procurementLedger.js";
  import useFormData from "@/hooks/useFormData.js";
  import QRCode from "qrcode";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const productData = ref([]);
  const selectedRows = ref([]);
  const productSelectedRows = ref([]);
  const modelOptions = ref([]);
  const userList = ref([]);
  const productOptions = ref([]);
  const salesContractList = ref([]);
  const supplierList = ref([]);
  const tableLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
  });
  const total = ref(0);
  const fileList = ref([]);
  import useUserStore from "@/store/modules/user";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import dayjs from "dayjs";
  import { getCurrentDate } from "@/utils/index.js";
const { proxy } = getCurrentInstance();
const tableData = ref([]);
const productData = ref([]);
const selectedRows = ref([]);
const productSelectedRows = ref([]);
const modelOptions = ref([]);
const userList = ref([]);
const productOptions = ref([]);
const salesContractList = ref([]);
const supplierList = ref([]);
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
});
const total = ref(0);
const fileList = ref([]);
import useUserStore from "@/store/modules/user";
import { modelList, productTreeList } from "@/api/basicData/product.js";
import dayjs from "dayjs";
import { getCurrentDate } from "@/utils/index.js";
  const userStore = useUserStore();
const userStore = useUserStore();
  // äºŒç»´ç ç›¸å…³å˜é‡
  const qrCodeDialogVisible = ref(false);
  const qrCodeUrl = ref("");
// äºŒç»´ç ç›¸å…³å˜é‡
const qrCodeDialogVisible = ref(false);
const qrCodeUrl = ref("");
  // è®¢å•审批状态显示文本
  const approvalStatusText = {
    0: "待审批",
    1: "审批通过",
    2: "审批失败",
  };
// è®¢å•审批状态显示文本
const approvalStatusText = {
  0: '待审批',
  1: '审批通过',
  2: '审批失败'
};
  // ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
  const operationType = ref("");
  const dialogFormVisible = ref(false);
  const data = reactive({
    searchForm: {
      supplierName: "", // ä¾›åº”商名称
      purchaseContractNumber: "", // é‡‡è´­åˆåŒç¼–号
      salesContractNo: "", // é”€å”®åˆåŒç¼–号
      projectName: "", // é¡¹ç›®åç§°
      entryDate: null, // å½•入日期
      entryDateStart: undefined,
      entryDateEnd: undefined,
    },
    form: {
      purchaseContractNumber: "",
      salesLedgerId: "",
      projectName: "",
      recorderId: "",
      entryDate: "",
      productData: [],
      supplierName: "",
      supplierId: "",
      paymentMethod: "",
      executionDate: "",
      approvalStatus: "0",
    },
    rules: {
      purchaseContractNumber: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      projectName: [{ required: true, message: "请输入", trigger: "blur" }],
      supplierId: [{ required: true, message: "请输入", trigger: "blur" }],
      entryDate: [{ required: true, message: "请选择", trigger: "change" }],
      executionDate: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { form, rules } = toRefs(data);
  const { form: searchForm } = useFormData(data.searchForm);
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
const dialogFormVisible = ref(false);
const data = reactive({
  searchForm: {
    supplierName: "", // ä¾›åº”商名称
    purchaseContractNumber: "", // é‡‡è´­åˆåŒç¼–号
    salesContractNo: "", // é”€å”®åˆåŒç¼–号
    projectName: "", // é¡¹ç›®åç§°
    entryDate: null, // å½•入日期
    entryDateStart: undefined,
    entryDateEnd: undefined,
  },
  form: {
    purchaseContractNumber: "",
    salesLedgerId: "",
    projectName: "",
    recorderId: "",
    entryDate: "",
    productData: [],
    supplierName: "",
    supplierId: "",
    paymentMethod: "",
        executionDate: "",
    approvalStatus: "0",
  },
  rules: {
    purchaseContractNumber: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    projectName: [{ required: true, message: "请输入", trigger: "blur" }],
    supplierId: [{ required: true, message: "请输入", trigger: "blur" }],
        entryDate: [{ required: true, message: "请选择", trigger: "change" }],
        executionDate: [{ required: true, message: "请选择", trigger: "change" }],
  },
});
const {  form, rules } = toRefs(data);
const { form: searchForm } = useFormData(data.searchForm);
  // äº§å“è¡¨å•弹框数据
  const productFormVisible = ref(false);
  const productOperationType = ref("");
  const productOperationIndex = ref("");
  const currentId = ref("");
  const productFormData = reactive({
    productForm: {
      productId: "",
      productCategory: "",
      productModelId: "",
      specificationModel: "",
      unit: "",
      quantity: "",
      taxInclusiveUnitPrice: "",
      taxRate: "",
      taxInclusiveTotalPrice: "",
      taxExclusiveTotalPrice: "",
      invoiceType: "",
      warnNum: "",
    },
    productRules: {
      productId: [{ required: true, message: "请选择", trigger: "change" }],
      productModelId: [{ required: true, message: "请选择", trigger: "change" }],
      unit: [{ required: true, message: "请输入", trigger: "blur" }],
      quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      taxInclusiveUnitPrice: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      taxRate: [{ required: true, message: "请选择", trigger: "change" }],
      warnNum: [{ required: false, message: "请选择", trigger: "change" }],
      taxInclusiveTotalPrice: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      taxExclusiveTotalPrice: [
        { required: true, message: "请输入", trigger: "blur" },
      ],
      invoiceType: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  const { productForm, productRules } = toRefs(productFormData);
  // const upload = reactive({
  //   // ä¸Šä¼ çš„地址
  //   url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  //   // è®¾ç½®ä¸Šä¼ çš„请求头部
  //   headers: { Authorization: "Bearer " + getToken() },
  // });
// äº§å“è¡¨å•弹框数据
const productFormVisible = ref(false);
const productOperationType = ref("");
const productOperationIndex = ref("");
const currentId = ref("");
const productFormData = reactive({
  productForm: {
    productId: "",
    productCategory: "",
    productModelId: "",
    specificationModel: "",
    unit: "",
    quantity: "",
    taxInclusiveUnitPrice: "",
    taxRate: "",
    taxInclusiveTotalPrice: "",
    taxExclusiveTotalPrice: "",
    invoiceType: "",
        warnNum: "",
  },
  productRules: {
    productId: [{ required: true, message: "请选择", trigger: "change" }],
    productModelId: [{ required: true, message: "请选择", trigger: "change" }],
    unit: [{ required: true, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    taxInclusiveUnitPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    taxRate: [{ required: true, message: "请选择", trigger: "change" }],
        warnNum: [{ required: false, message: "请选择", trigger: "change" }],
    taxInclusiveTotalPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    taxExclusiveTotalPrice: [
      { required: true, message: "请输入", trigger: "blur" },
    ],
    invoiceType: [{ required: true, message: "请选择", trigger: "change" }],
  },
});
const { productForm, productRules } = toRefs(productFormData);
const upload = reactive({
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
});
  const changeDaterange = value => {
    if (value) {
      searchForm.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
      searchForm.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
    } else {
      searchForm.entryDateStart = undefined;
      searchForm.entryDateEnd = undefined;
    }
    handleQuery();
  };
const changeDaterange = (value) => {
  if (value) {
    searchForm.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
    searchForm.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
  } else {
    searchForm.entryDateStart = undefined;
    searchForm.entryDateEnd = undefined;
  }
  handleQuery();
};
const formattedNumber = (row, column, cellValue) => {
  return parseFloat(cellValue).toFixed(2);
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1;
  getList();
};
// å­è¡¨åˆè®¡æ–¹æ³•
const summarizeChildrenTable = (param) => {
  return proxy.summarizeTable(
    param,
    [
  const formattedNumber = (row, column, cellValue) => {
    return parseFloat(cellValue).toFixed(2);
  };
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  // å­è¡¨åˆè®¡æ–¹æ³•
  const summarizeChildrenTable = param => {
    return proxy.summarizeTable(
      param,
      [
        "taxInclusiveUnitPrice",
        "taxInclusiveTotalPrice",
        "taxExclusiveTotalPrice",
        "ticketsNum",
        "ticketsAmount",
        "futureTickets",
        "futureTicketsAmount",
      ],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      }
    );
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    const { entryDate, ...rest } = searchForm;
    purchaseListPage({ ...rest, ...page })
      .then(res => {
        tableLoading.value = false;
        // tableData.value = res.data.records;
        // å¤„理数据,添加失效状态标记
        tableData.value = res.data.records.map(record => ({
          ...record,
          isInvalid: record.isWhite === 1,
        }));
        tableData.value.map(item => {
          item.children = [];
        });
        total.value = res.data.total;
        expandedRowKeys.value = [];
      })
      .catch(() => {
        tableLoading.value = false;
      });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  const productSelected = selectedRows => {
    productSelectedRows.value = selectedRows;
  };
  const expandedRowKeys = ref([]);
  // å±•开行
  const expandChange = (row, expandedRows) => {
    if (expandedRows.length > 0) {
      expandedRowKeys.value = [];
      try {
        productList({ salesLedgerId: row.id, type: 2 }).then(res => {
          const index = tableData.value.findIndex(item => item.id === row.id);
          if (index > -1) {
            tableData.value[index].children = res.data;
          }
          expandedRowKeys.value.push(row.id);
        });
      } catch (error) {
        console.log(error);
      }
    } else {
      expandedRowKeys.value = [];
    }
  };
  // ä¸»è¡¨åˆè®¡æ–¹æ³•
  const summarizeMainTable = param => {
    return proxy.summarizeTable(param, ["contractAmount"]);
  };
  // å­è¡¨åˆè®¡æ–¹æ³•
  const summarizeProTable = param => {
    return proxy.summarizeTable(param, [
      "taxInclusiveUnitPrice",
      "taxInclusiveTotalPrice",
      "taxExclusiveTotalPrice",
      "ticketsNum",
      "ticketsAmount",
      "futureTickets",
      "futureTicketsAmount",
    ],
    {
      ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
    }
  );
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const getList = () => {
  tableLoading.value = true;
  const { entryDate, ...rest } = searchForm;
  purchaseListPage({ ...rest, ...page })
    .then((res) => {
      tableLoading.value = false;
      // tableData.value = res.data.records;
      // å¤„理数据,添加失效状态标记
      tableData.value = res.data.records.map(record => ({
        ...record,
        isInvalid: record.isWhite === 1
      }));
      tableData.value.map((item) => {
        item.children = [];
    ]);
  };
  // æ‰“开弹框
  const openForm = (type, row) => {
    operationType.value = type;
    form.value = {};
    productData.value = [];
    fileList.value = [];
    if (operationType.value == "add") {
      createPurchaseNo().then(res => {
        form.value.purchaseContractNumber = res.data;
      });
      total.value = res.data.total;
      expandedRowKeys.value = [];
    })
    .catch(() => {
      tableLoading.value = false;
    }
    userListNoPage().then(res => {
      userList.value = res.data;
    });
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
const productSelected = (selectedRows) => {
  productSelectedRows.value = selectedRows;
};
const expandedRowKeys = ref([]);
// å±•开行
const expandChange = (row, expandedRows) => {
  if (expandedRows.length > 0) {
    expandedRowKeys.value = [];
    try {
      productList({ salesLedgerId: row.id, type: 2 }).then((res) => {
        const index = tableData.value.findIndex((item) => item.id === row.id);
        if (index > -1) {
          tableData.value[index].children = res.data;
    getSalesNo().then(res => {
      salesContractList.value = res;
    });
    getOptions().then(res => {
      // ä¾›åº”商过滤出isWhite=0 çš„æ•°æ®
      supplierList.value = res.data.filter(item => item.isWhite == 0);
    });
    form.value.recorderId = userStore.id;
    form.value.entryDate = getCurrentDate();
    if (type === "edit") {
      currentId.value = row.id;
      getPurchaseById({ id: row.id, type: 2 }).then(res => {
        form.value = { ...res };
        productData.value = form.value.productData;
        if (form.value.salesLedgerFiles) {
          fileList.value = form.value.salesLedgerFiles;
        } else {
          fileList.value = [];
        }
        expandedRowKeys.value.push(row.id);
      });
    } catch (error) {
      console.log(error);
    }
  } else {
    expandedRowKeys.value = [];
    dialogFormVisible.value = true;
  };
  // ä¸Šä¼ å‰æ ¡æ£€
  function handleBeforeUpload(file) {
    // æ ¡æ£€æ–‡ä»¶å¤§å°
    if (file.size > 1024 * 1024 * 10) {
      proxy.$modal.msgError("上传文件大小不能超过10MB!");
      return false;
    }
    proxy.$modal.loading("正在上传文件,请稍候...");
    return true;
  }
};
// ä¸»è¡¨åˆè®¡æ–¹æ³•
const summarizeMainTable = (param) => {
  return proxy.summarizeTable(param, ["contractAmount"]);
};
// å­è¡¨åˆè®¡æ–¹æ³•
const summarizeProTable = (param) => {
  return proxy.summarizeTable(param, [
    "taxInclusiveUnitPrice",
    "taxInclusiveTotalPrice",
    "taxExclusiveTotalPrice",
  ]);
};
// æ‰“开弹框
const openForm = (type, row) => {
  operationType.value = type;
  form.value = {};
  productData.value = [];
  fileList.value = [];
  if (operationType.value == "add") {
    createPurchaseNo().then((res) => {
      form.value.purchaseContractNumber = res.data;
    });
  // ä¸Šä¼ å¤±è´¥
  function handleUploadError(err) {
    proxy.$modal.msgError("上传文件失败");
    proxy.$modal.closeLoading();
  }
  userListNoPage().then((res) => {
    userList.value = res.data;
  });
  getSalesNo().then((res) => {
    salesContractList.value = res;
  });
  getOptions().then((res) => {
    // ä¾›åº”商过滤出isWhite=0 çš„æ•°æ®
    supplierList.value = res.data.filter((item) => item.isWhite == 0);
  });
  form.value.recorderId = userStore.id;
  form.value.entryDate = getCurrentDate();
  if (type === "edit") {
    currentId.value = row.id;
    getPurchaseById({ id: row.id, type: 2 }).then((res) => {
      form.value = { ...res };
      productData.value = form.value.productData;
      if (form.value.salesLedgerFiles) {
        fileList.value = form.value.salesLedgerFiles;
      } else {
        fileList.value = [];
  // ä¸Šä¼ æˆåŠŸå›žè°ƒ
  function handleUploadSuccess(res, file, uploadFiles) {
    proxy.$modal.closeLoading();
    if (res.code === 200) {
      file.tempId = res.data.tempId;
      proxy.$modal.msgSuccess("上传成功");
    } else {
      proxy.$modal.msgError(res.msg);
      proxy.$refs.fileUpload.handleRemove(file);
    }
  }
  // ç§»é™¤æ–‡ä»¶
  function handleRemove(file) {
    console.log("handleRemove", file.id);
    if (file.size > 1024 * 1024 * 10) {
      // ä»…前端清理,不调用删除接口和提示
      return;
    }
    if (operationType.value === "edit") {
      let ids = [];
      ids.push(file.id);
      delLedgerFile(ids).then(res => {
        proxy.$modal.msgSuccess("删除成功");
      });
    }
  }
  // æäº¤è¡¨å•
  const submitForm = n => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        if (productData.value.length > 0) {
          form.value.productData = proxy.HaveJson(productData.value);
        } else {
          proxy.$modal.msgWarning("请添加产品信息");
          return;
        }
        let tempFileIds = [];
        if (fileList.value.length > 0) {
          tempFileIds = fileList.value.map(item => item.tempId);
        }
        form.value.tempFileIds = tempFileIds;
        form.value.type = 2;
        form.value.approvalStatus = n;
        addOrEditPurchase(form.value).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
          getList();
        });
      }
    });
  }
  dialogFormVisible.value = true;
};
// ä¸Šä¼ å‰æ ¡æ£€
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) {
    file.tempId = res.data.tempId;
    proxy.$modal.msgSuccess("上传成功");
  } else {
    proxy.$modal.msgError(res.msg);
    proxy.$refs.fileUpload.handleRemove(file);
  }
}
// ç§»é™¤æ–‡ä»¶
function handleRemove(file) {
  console.log("handleRemove", file.id);
  if (file.size > 1024 * 1024 * 10) {
    // ä»…前端清理,不调用删除接口和提示
    return;
  }
  if (operationType.value === "edit") {
    let ids = [];
    ids.push(file.id);
    delLedgerFile(ids).then((res) => {
      proxy.$modal.msgSuccess("删除成功");
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
  };
  // æ‰“开产品弹框
  const openProductForm = (type, row, index) => {
    productOperationType.value = type;
    productOperationIndex.value = index;
    productForm.value = {};
    proxy.resetForm("productFormRef");
    if (type === "edit") {
      productForm.value = { ...row };
    }
    productFormVisible.value = true;
    getProductOptions();
  };
  const getProductOptions = () => {
    productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
    });
  };
  const getModels = value => {
    if (value) {
      productForm.value.productCategory =
        findNodeById(productOptions.value, value) || "";
      modelList({ id: value }).then(res => {
        modelOptions.value = res;
      });
    } else {
      productForm.value.productCategory = "";
      modelOptions.value = [];
    }
  };
  const getProductModel = value => {
    const index = modelOptions.value.findIndex(item => item.id === value);
    if (index !== -1) {
      productForm.value.specificationModel = modelOptions.value[index].model;
      productForm.value.unit = modelOptions.value[index].unit;
    } else {
      productForm.value.specificationModel = null;
      productForm.value.unit = null;
    }
  };
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹çš„label
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œç›´æŽ¥è¿”å›žï¼ˆå·²ç»æ˜¯label字符串)
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
}
// æäº¤è¡¨å•
const submitForm = (n) => {
  proxy.$refs["formRef"].validate((valid) => {
    if (valid) {
      if (productData.value.length > 0) {
        form.value.productData = proxy.HaveJson(productData.value);
      } else {
        proxy.$modal.msgWarning("请添加产品信息");
  // æäº¤äº§å“è¡¨å•
  const submitProduct = () => {
    proxy.$refs["productFormRef"].validate(valid => {
      if (valid) {
        if (operationType.value === "edit") {
          submitProductEdit();
        } else {
          if (productOperationType.value === "add") {
            productData.value.push({ ...productForm.value });
            console.log("productData.value---", productData.value);
          } else {
            productData.value[productOperationIndex.value] = {
              ...productForm.value,
            };
          }
          closeProductDia();
        }
      }
    });
  };
  const submitProductEdit = () => {
    productForm.value.salesLedgerId = currentId.value;
    productForm.value.type = 2;
    addOrUpdateSalesLedgerProduct(productForm.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
      closeProductDia();
      getPurchaseById({ id: currentId.value, type: 2 }).then(res => {
        productData.value = res.productData;
      });
    });
  };
  // åˆ é™¤äº§å“
  const deleteProduct = () => {
    if (productSelectedRows.value.length === 0) {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    if (operationType.value === "add") {
      productSelectedRows.value.forEach(selectedRow => {
        const index = productData.value.findIndex(
          product => product.id === selectedRow.id
        );
        if (index !== -1) {
          productData.value.splice(index, 1);
        }
      });
    } else {
      let ids = [];
      if (productSelectedRows.value.length > 0) {
        ids = productSelectedRows.value.map(item => item.id);
      }
      ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          delProduct(ids).then(res => {
            proxy.$modal.msgSuccess("删除成功");
            closeProductDia();
            getSalesLedgerWithProducts({ id: currentId.value, type: 2 }).then(
              res => {
                productData.value = res.productData;
              }
            );
          });
        })
        .catch(() => {
          proxy.$modal.msg("已取消");
        });
    }
  };
  // å…³é—­äº§å“å¼¹æ¡†
  const closeProductDia = () => {
    proxy.resetForm("productFormRef");
    productFormVisible.value = false;
  };
  // å®¡æ‰¹é€šè¿‡æ–¹æ³•
  const approvePurchase = row => {
    ElMessageBox.confirm(
      `确认通过采购合同号为 ${row.purchaseContractNumber} çš„审批?`,
      "审批确认",
      {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(() => {
        updateApprovalStatus({ id: row.id, approvalStatus: 1 }).then(res => {
          proxy.$modal.msgSuccess("审批成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消审批");
      });
  };
  // å®¡æ‰¹æ‹’绝方法
  const rejectPurchase = row => {
    ElMessageBox.confirm(
      `确认拒绝采购合同号为 ${row.purchaseContractNumber} çš„审批?`,
      "审批确认",
      {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
      }
    )
      .then(() => {
        updateApprovalStatus({ id: row.id, approvalStatus: 2 }).then(res => {
          proxy.$modal.msgSuccess("审批成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消审批");
      });
  };
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download("/purchase/ledger/export", {}, "采购台账.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  // åˆ é™¤
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»–人维护的数据
      const unauthorizedData = selectedRows.value.filter(
        item => item.recorderName !== userStore.nickName
      );
      if (unauthorizedData.length > 0) {
        proxy.$modal.msgWarning("不可删除他人维护的数据");
        return;
      }
      let tempFileIds = [];
      if (fileList.value.length > 0) {
        tempFileIds = fileList.value.map((item) => item.tempId);
      }
      form.value.tempFileIds = tempFileIds;
      form.value.type = 2;
      form.value.approvalStatus = n;
      addOrEditPurchase(form.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeDia();
        getList();
      });
    }
  });
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
};
// æ‰“开产品弹框
const openProductForm = (type, row, index) => {
  productOperationType.value = type;
  productOperationIndex.value = index;
  productForm.value = {};
  proxy.resetForm("productFormRef");
  if (type === "edit") {
    productForm.value = { ...row };
  }
  productFormVisible.value = true;
  getProductOptions();
};
const getProductOptions = () => {
  productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
  });
};
const getModels = (value) => {
  if (value) {
    productForm.value.productCategory = findNodeById(productOptions.value, value) || "";
    modelList({ id: value }).then((res) => {
      modelOptions.value = res;
    });
  } else {
    productForm.value.productCategory = "";
    modelOptions.value = [];
  }
};
const getProductModel = (value) => {
  const index = modelOptions.value.findIndex((item) => item.id === value);
  if (index !== -1) {
    productForm.value.specificationModel = modelOptions.value[index].model;
    productForm.value.unit = modelOptions.value[index].unit;
  } else {
    productForm.value.specificationModel = null;
    productForm.value.unit = null;
  }
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
      return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹çš„label
    }
    if (nodes[i].children && nodes[i].children.length > 0) {
      const foundNode = findNodeById(nodes[i].children, productId);
      if (foundNode) {
        return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œç›´æŽ¥è¿”å›žï¼ˆå·²ç»æ˜¯label字符串)
      }
    }
  }
  return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
};
function convertIdToValue(data) {
  return data.map((item) => {
    const { id, children, ...rest } = item;
    const newItem = {
      ...rest,
      value: id, // å°† id æ”¹ä¸º value
    };
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
// æäº¤äº§å“è¡¨å•
const submitProduct = () => {
  proxy.$refs["productFormRef"].validate((valid) => {
    if (valid) {
      if (operationType.value === "edit") {
        submitProductEdit();
      } else {
        if (productOperationType.value === "add") {
          productData.value.push({ ...productForm.value });
          console.log("productData.value---", productData.value);
        } else {
          productData.value[productOperationIndex.value] = {
            ...productForm.value,
          };
        }
        closeProductDia();
      }
    }
  });
};
const submitProductEdit = () => {
  productForm.value.salesLedgerId = currentId.value;
  productForm.value.type = 2;
  addOrUpdateSalesLedgerProduct(productForm.value).then((res) => {
    proxy.$modal.msgSuccess("提交成功");
    closeProductDia();
    getPurchaseById({ id: currentId.value, type: 2 }).then((res) => {
      productData.value = res.productData;
    });
  });
};
// åˆ é™¤äº§å“
const deleteProduct = () => {
  if (productSelectedRows.value.length === 0) {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  if (operationType.value === "add") {
    productSelectedRows.value.forEach((selectedRow) => {
      const index = productData.value.findIndex(
        (product) => product.id === selectedRow.id
      );
      if (index !== -1) {
        productData.value.splice(index, 1);
      }
    });
  } else {
    let ids = [];
    if (productSelectedRows.value.length > 0) {
      ids = productSelectedRows.value.map((item) => item.id);
      ids = selectedRows.value.map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
      confirmButtonText: "确认",
@@ -681,384 +774,313 @@
      type: "warning",
    })
      .then(() => {
        delProduct(ids).then((res) => {
        delPurchase(ids).then(res => {
          proxy.$modal.msgSuccess("删除成功");
          closeProductDia();
          getSalesLedgerWithProducts({ id: currentId.value, type: 2 }).then(
            (res) => {
              productData.value = res.productData;
            }
          );
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  }
};
// å…³é—­äº§å“å¼¹æ¡†
const closeProductDia = () => {
  proxy.resetForm("productFormRef");
  productFormVisible.value = false;
};
// å®¡æ‰¹é€šè¿‡æ–¹æ³•
const approvePurchase = (row) => {
  ElMessageBox.confirm(`确认通过采购合同号为 ${row.purchaseContractNumber} çš„审批?`, '审批确认', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
  }).then(() => {
    updateApprovalStatus({ id: row.id, approvalStatus: 1}).then((res)=>{
      proxy.$modal.msgSuccess('审批成功');
      getList();
    })
  }).catch(() => {
    proxy.$modal.msg('已取消审批');
  });
};
// å®¡æ‰¹æ‹’绝方法
const rejectPurchase = (row) => {
  ElMessageBox.confirm(`确认拒绝采购合同号为 ${row.purchaseContractNumber} çš„审批?`, '审批确认', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
  }).then(() => {
    updateApprovalStatus({ id: row.id, approvalStatus: 2}).then((res)=>{
      proxy.$modal.msgSuccess('审批成功');
      getList();
    })
  }).catch(() => {
    proxy.$modal.msg('已取消审批');
  });
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download("/purchase/ledger/export", {}, "采购台账.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
// åˆ é™¤
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
        // æ£€æŸ¥æ˜¯å¦æœ‰ä»–人维护的数据
        const unauthorizedData = selectedRows.value.filter(item => item.recorderName !== userStore.nickName);
        if (unauthorizedData.length > 0) {
            proxy.$modal.msgWarning("不可删除他人维护的数据");
            return;
        }
    ids = selectedRows.value.map((item) => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
  }
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      delPurchase(ids).then((res) => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
const mathNum = () => {
    if (!productForm.value.taxRate) {
        proxy.$modal.msgWarning("请先选择税率");
        return;
    }
  if (!productForm.value.taxInclusiveUnitPrice) {
    return;
  }
  if (!productForm.value.quantity) {
    return;
  }
  // å«ç¨Žæ€»ä»·è®¡ç®—
  productForm.value.taxInclusiveTotalPrice =
    proxy.calculateTaxIncludeTotalPrice(
      productForm.value.taxInclusiveUnitPrice,
      productForm.value.quantity
    );
  if (productForm.value.taxRate) {
    // ä¸å«ç¨Žæ€»ä»·è®¡ç®—
    productForm.value.taxExclusiveTotalPrice =
      proxy.calculateTaxExclusiveTotalPrice(
        productForm.value.taxInclusiveTotalPrice,
        productForm.value.taxRate
      );
  }
};
const reverseMathNum = (field) => {
    if (!productForm.value.taxRate) {
        proxy.$modal.msgWarning("请先选择税率");
        return;
    }
  const taxRate = Number(productForm.value.taxRate);
  if (!taxRate) return;
  if (field === 'taxInclusiveTotalPrice') {
    // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œæ•°é‡ï¼Œåç®—含税单价
    if (productForm.value.quantity) {
      productForm.value.taxInclusiveUnitPrice =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity)).toFixed(2);
    }
    // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œå«ç¨Žå•价,反算数量
    else if (productForm.value.taxInclusiveUnitPrice) {
      productForm.value.quantity =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.taxInclusiveUnitPrice)).toFixed(2);
    }
    // åç®—不含税总价
    productForm.value.taxExclusiveTotalPrice =
      (Number(productForm.value.taxInclusiveTotalPrice) / (1 + taxRate / 100)).toFixed(2);
  } else if (field === 'taxExclusiveTotalPrice') {
    // åç®—含税总价
    productForm.value.taxInclusiveTotalPrice =
      (Number(productForm.value.taxExclusiveTotalPrice) * (1 + taxRate / 100)).toFixed(2);
    // å·²çŸ¥æ•°é‡ï¼Œåç®—含税单价
    if (productForm.value.quantity) {
      productForm.value.taxInclusiveUnitPrice =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity)).toFixed(2);
    }
    // å·²çŸ¥å«ç¨Žå•价,反算数量
    else if (productForm.value.taxInclusiveUnitPrice) {
      productForm.value.quantity =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.taxInclusiveUnitPrice)).toFixed(2);
    }
  }
};
// é”€å”®åˆåŒé€‰æ‹©æ”¹å˜æ–¹æ³•
const salesLedgerChange = async (row) => {
  console.log("row", row);
  var index = salesContractList.value.findIndex((item) => item.id == row);
  console.log("index", index);
  if (index > -1) {
    form.value.projectName = salesContractList.value[index].projectName;
    await querygProductInfoByContractNo();
  }
};
const querygProductInfoByContractNo = async () => {
  const { code, data } = await getProductInfoByContractNo({
    contractNo: form.value.salesLedgerId,
  });
  if (code == 200) {
    productData.value = data;
  }
};
// æ˜¾ç¤ºäºŒç»´ç 
const showQRCode = async (row) => {
  try {
    // æž„建二维码内容,只包含采购合同号(纯文本)
    const qrContent = row.purchaseContractNumber || '';
    // æ£€æŸ¥å†…容是否为空
    if (!qrContent || qrContent.trim() === '') {
      proxy.$modal.msgWarning("该行没有采购合同号,无法生成二维码");
  };
  const mathNum = () => {
    if (!productForm.value.taxRate) {
      proxy.$modal.msgWarning("请先选择税率");
      return;
    }
    qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
      width: 200,
      margin: 2,
      color: {
        dark: '#000000',
        light: '#FFFFFF'
    if (!productForm.value.taxInclusiveUnitPrice) {
      return;
    }
    if (!productForm.value.quantity) {
      return;
    }
    // å«ç¨Žæ€»ä»·è®¡ç®—
    productForm.value.taxInclusiveTotalPrice =
      proxy.calculateTaxIncludeTotalPrice(
        productForm.value.taxInclusiveUnitPrice,
        productForm.value.quantity
      );
    if (productForm.value.taxRate) {
      // ä¸å«ç¨Žæ€»ä»·è®¡ç®—
      productForm.value.taxExclusiveTotalPrice =
        proxy.calculateTaxExclusiveTotalPrice(
          productForm.value.taxInclusiveTotalPrice,
          productForm.value.taxRate
        );
    }
  };
  const reverseMathNum = field => {
    if (!productForm.value.taxRate) {
      proxy.$modal.msgWarning("请先选择税率");
      return;
    }
    const taxRate = Number(productForm.value.taxRate);
    if (!taxRate) return;
    if (field === "taxInclusiveTotalPrice") {
      // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œæ•°é‡ï¼Œåç®—含税单价
      if (productForm.value.quantity) {
        productForm.value.taxInclusiveUnitPrice = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.quantity)
        ).toFixed(2);
      }
      // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œå«ç¨Žå•价,反算数量
      else if (productForm.value.taxInclusiveUnitPrice) {
        productForm.value.quantity = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.taxInclusiveUnitPrice)
        ).toFixed(2);
      }
      // åç®—不含税总价
      productForm.value.taxExclusiveTotalPrice = (
        Number(productForm.value.taxInclusiveTotalPrice) /
        (1 + taxRate / 100)
      ).toFixed(2);
    } else if (field === "taxExclusiveTotalPrice") {
      // åç®—含税总价
      productForm.value.taxInclusiveTotalPrice = (
        Number(productForm.value.taxExclusiveTotalPrice) *
        (1 + taxRate / 100)
      ).toFixed(2);
      // å·²çŸ¥æ•°é‡ï¼Œåç®—含税单价
      if (productForm.value.quantity) {
        productForm.value.taxInclusiveUnitPrice = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.quantity)
        ).toFixed(2);
      }
      // å·²çŸ¥å«ç¨Žå•价,反算数量
      else if (productForm.value.taxInclusiveUnitPrice) {
        productForm.value.quantity = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.taxInclusiveUnitPrice)
        ).toFixed(2);
      }
    }
  };
  // é”€å”®åˆåŒé€‰æ‹©æ”¹å˜æ–¹æ³•
  const salesLedgerChange = async row => {
    console.log("row", row);
    var index = salesContractList.value.findIndex(item => item.id == row);
    console.log("index", index);
    if (index > -1) {
      form.value.projectName = salesContractList.value[index].projectName;
      await querygProductInfoByContractNo();
    }
  };
  const querygProductInfoByContractNo = async () => {
    const { code, data } = await getProductInfoByContractNo({
      contractNo: form.value.salesLedgerId,
    });
    if (code == 200) {
      productData.value = data;
    }
  };
  // æ˜¾ç¤ºäºŒç»´ç 
  const showQRCode = async row => {
    try {
      // æž„建二维码内容,只包含采购合同号(纯文本)
      const qrContent = row.purchaseContractNumber || "";
      // æ£€æŸ¥å†…容是否为空
      if (!qrContent || qrContent.trim() === "") {
        proxy.$modal.msgWarning("该行没有采购合同号,无法生成二维码");
        return;
      }
      qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
        width: 200,
        margin: 2,
        color: {
          dark: "#000000",
          light: "#FFFFFF",
        },
      });
      qrCodeDialogVisible.value = true;
    } catch (error) {
      console.error("生成二维码失败:", error);
      proxy.$modal.msgError("生成二维码失败:" + error.message);
    }
  };
  // ä¸‹è½½äºŒç»´ç 
  const downloadQRCode = () => {
    if (!qrCodeUrl.value) {
      proxy.$modal.msgWarning("二维码未生成");
      return;
    }
    const a = document.createElement("a");
    a.href = qrCodeUrl.value;
    a.download = `采购合同号二维码_${new Date().getTime()}.png`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    proxy.$modal.msgSuccess("下载成功");
  };
  // æ‰«ç æ–°å¢žå¯¹è¯æ¡†ç›¸å…³å˜é‡
  const scanAddDialogVisible = ref(false);
  const scanAddForm = reactive({
    scanContent: "",
    purchaseContractNumber: "",
    supplierName: "",
    projectName: "",
    contractAmount: "",
    paymentMethod: "",
    recorderName: "",
    scanRemark: "",
  });
  const scanAddRules = {
    purchaseContractNumber: [
      { required: true, message: "请输入采购合同号", trigger: "blur" },
    ],
    supplierName: [
      { required: true, message: "请输入供应商名称", trigger: "blur" },
    ],
    projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
  };
  // æ‰«ç ç™»è®°å¯¹è¯æ¡†ç›¸å…³å˜é‡
  const scanDialogVisible = ref(false);
  const scanForm = reactive({
    purchaseContractNumber: "",
    supplierName: "",
    projectName: "",
    scanTime: "",
    scannerName: "",
    scanStatus: "未扫码",
    scanRemark: "",
  });
  const scanRules = {
    scanRemark: [{ required: true, message: "请输入扫码备注", trigger: "blur" }],
  };
  const scanRecords = ref([]);
  // æ‰“开扫码新增对话框
  const openScanAddDialog = () => {
    scanAddForm.scanContent = "";
    scanAddForm.purchaseContractNumber = "";
    scanAddForm.supplierName = "";
    scanAddForm.projectName = "";
    scanAddForm.contractAmount = "";
    scanAddForm.paymentMethod = "";
    scanAddForm.recorderName = userStore.nickName;
    scanAddForm.scanRemark = "";
    scanAddDialogVisible.value = true;
  };
  // è§£æžæ‰«ç å†…容(模拟解析二维码数据)
  const parseScanContent = content => {
    if (!content) return;
    // æ¨¡æ‹Ÿè§£æžäºŒç»´ç å†…容,这里可以根据实际需求调整解析逻辑
    // å‡è®¾æ‰«ç å†…容格式为:合同号|供应商|项目|金额|付款方式
    const parts = content.split("|");
    if (parts.length >= 3) {
      scanAddForm.purchaseContractNumber = parts[0] || "";
      scanAddForm.supplierName = parts[1] || "";
      scanAddForm.projectName = parts[2] || "";
      scanAddForm.contractAmount = parts[3] || "";
      scanAddForm.paymentMethod = parts[4] || "";
    }
  };
  // å…³é—­æ‰«ç æ–°å¢žå¯¹è¯æ¡†
  const closeScanAddDialog = () => {
    scanAddDialogVisible.value = false;
    proxy.resetForm("scanAddFormRef");
  };
  // æäº¤æ‰«ç æ–°å¢ž
  const submitScanAdd = () => {
    proxy.$refs["scanAddFormRef"].validate(valid => {
      if (valid) {
        // æž„建新增数据
        const newData = {
          purchaseContractNumber: scanAddForm.purchaseContractNumber,
          supplierName: scanAddForm.supplierName,
          projectName: scanAddForm.projectName,
          contractAmount: scanAddForm.contractAmount,
          paymentMethod: scanAddForm.paymentMethod,
          recorderName: scanAddForm.recorderName,
          entryDate: getCurrentDate(),
          remark: scanAddForm.scanRemark,
          type: 2,
        };
        // æ¨¡æ‹Ÿæ–°å¢žæˆåŠŸ
        proxy.$modal.msgSuccess("扫码新增成功!");
        closeScanAddDialog();
        // å¯ä»¥é€‰æ‹©æ˜¯å¦åˆ·æ–°åˆ—表
        // getList();
      }
    });
    qrCodeDialogVisible.value = true;
  } catch (error) {
    console.error('生成二维码失败:', error);
    proxy.$modal.msgError("生成二维码失败:" + error.message);
  };
  // æ‰“开扫码登记对话框
  const openScanDialog = row => {
    scanForm.purchaseContractNumber = row.purchaseContractNumber;
    scanForm.supplierName = row.supplierName;
    scanForm.projectName = row.projectName;
    scanForm.scanTime = getCurrentDateTime();
    scanForm.scannerName = userStore.nickName;
    scanForm.scanStatus = "未扫码";
    scanForm.scanRemark = "";
    scanRecords.value = [];
    scanDialogVisible.value = true;
  };
  // å…³é—­æ‰«ç ç™»è®°å¯¹è¯æ¡†
  const closeScanDialog = () => {
    scanDialogVisible.value = false;
    proxy.resetForm("scanFormRef");
  };
  // æäº¤æ‰«ç ç™»è®°
  const submitScan = () => {
    proxy.$refs["scanFormRef"].validate(valid => {
      if (valid) {
        // æ·»åŠ æ‰«ç è®°å½•
        scanRecords.value.push({
          ...scanForm,
          id: Date.now(), // æ¨¡æ‹ŸID
          scanTime: getCurrentDateTime(),
        });
        scanForm.scanStatus = "已扫码";
        scanForm.scanRemark = scanForm.scanRemark || "无";
        proxy.$modal.msgSuccess("扫码登记成功!");
        closeScanDialog();
      }
    });
  };
  // èŽ·å–å½“å‰æ—¥æœŸæ—¶é—´
  function getCurrentDateTime() {
    const now = new Date();
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, "0");
    const day = String(now.getDate()).padStart(2, "0");
    const hours = String(now.getHours()).padStart(2, "0");
    const minutes = String(now.getMinutes()).padStart(2, "0");
    const seconds = String(now.getSeconds()).padStart(2, "0");
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  }
};
// ä¸‹è½½äºŒç»´ç 
const downloadQRCode = () => {
  if (!qrCodeUrl.value) {
    proxy.$modal.msgWarning("二维码未生成");
    return;
  }
  const a = document.createElement('a');
  a.href = qrCodeUrl.value;
  a.download = `采购合同号二维码_${new Date().getTime()}.png`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  proxy.$modal.msgSuccess("下载成功");
};
  // æ·»åŠ è¡Œç±»åæ–¹æ³•
  const tableRowClassName = ({ row }) => {
    return row.isInvalid ? "invalid-row" : "";
  };
// æ‰«ç æ–°å¢žå¯¹è¯æ¡†ç›¸å…³å˜é‡
const scanAddDialogVisible = ref(false);
const scanAddForm = reactive({
  scanContent: "",
  purchaseContractNumber: "",
  supplierName: "",
  projectName: "",
  contractAmount: "",
  paymentMethod: "",
  recorderName: "",
  scanRemark: "",
});
const scanAddRules = {
  purchaseContractNumber: [{ required: true, message: "请输入采购合同号", trigger: "blur" }],
  supplierName: [{ required: true, message: "请输入供应商名称", trigger: "blur" }],
  projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
};
// æ‰«ç ç™»è®°å¯¹è¯æ¡†ç›¸å…³å˜é‡
const scanDialogVisible = ref(false);
const scanForm = reactive({
  purchaseContractNumber: "",
  supplierName: "",
  projectName: "",
  scanTime: "",
  scannerName: "",
  scanStatus: "未扫码",
  scanRemark: "",
});
const scanRules = {
  scanRemark: [{ required: true, message: "请输入扫码备注", trigger: "blur" }],
};
const scanRecords = ref([]);
// æ‰“开扫码新增对话框
const openScanAddDialog = () => {
  scanAddForm.scanContent = "";
  scanAddForm.purchaseContractNumber = "";
  scanAddForm.supplierName = "";
  scanAddForm.projectName = "";
  scanAddForm.contractAmount = "";
  scanAddForm.paymentMethod = "";
  scanAddForm.recorderName = userStore.nickName;
  scanAddForm.scanRemark = "";
  scanAddDialogVisible.value = true;
};
// è§£æžæ‰«ç å†…容(模拟解析二维码数据)
const parseScanContent = (content) => {
  if (!content) return;
  // æ¨¡æ‹Ÿè§£æžäºŒç»´ç å†…容,这里可以根据实际需求调整解析逻辑
  // å‡è®¾æ‰«ç å†…容格式为:合同号|供应商|项目|金额|付款方式
  const parts = content.split('|');
  if (parts.length >= 3) {
    scanAddForm.purchaseContractNumber = parts[0] || "";
    scanAddForm.supplierName = parts[1] || "";
    scanAddForm.projectName = parts[2] || "";
    scanAddForm.contractAmount = parts[3] || "";
    scanAddForm.paymentMethod = parts[4] || "";
  }
};
// å…³é—­æ‰«ç æ–°å¢žå¯¹è¯æ¡†
const closeScanAddDialog = () => {
  scanAddDialogVisible.value = false;
  proxy.resetForm("scanAddFormRef");
};
// æäº¤æ‰«ç æ–°å¢ž
const submitScanAdd = () => {
  proxy.$refs["scanAddFormRef"].validate((valid) => {
    if (valid) {
      // æž„建新增数据
      const newData = {
        purchaseContractNumber: scanAddForm.purchaseContractNumber,
        supplierName: scanAddForm.supplierName,
        projectName: scanAddForm.projectName,
        contractAmount: scanAddForm.contractAmount,
        paymentMethod: scanAddForm.paymentMethod,
        recorderName: scanAddForm.recorderName,
        entryDate: getCurrentDate(),
        remark: scanAddForm.scanRemark,
        type: 2
      };
      // æ¨¡æ‹Ÿæ–°å¢žæˆåŠŸ
      proxy.$modal.msgSuccess("扫码新增成功!");
      closeScanAddDialog();
      // å¯ä»¥é€‰æ‹©æ˜¯å¦åˆ·æ–°åˆ—表
      // getList();
    }
  onMounted(() => {
    getList();
  });
};
// æ‰“开扫码登记对话框
const openScanDialog = (row) => {
  scanForm.purchaseContractNumber = row.purchaseContractNumber;
  scanForm.supplierName = row.supplierName;
  scanForm.projectName = row.projectName;
  scanForm.scanTime = getCurrentDateTime();
  scanForm.scannerName = userStore.nickName;
  scanForm.scanStatus = "未扫码";
  scanForm.scanRemark = "";
  scanRecords.value = [];
  scanDialogVisible.value = true;
};
// å…³é—­æ‰«ç ç™»è®°å¯¹è¯æ¡†
const closeScanDialog = () => {
  scanDialogVisible.value = false;
  proxy.resetForm("scanFormRef");
};
// æäº¤æ‰«ç ç™»è®°
const submitScan = () => {
  proxy.$refs["scanFormRef"].validate((valid) => {
    if (valid) {
      // æ·»åŠ æ‰«ç è®°å½•
      scanRecords.value.push({
        ...scanForm,
        id: Date.now(), // æ¨¡æ‹ŸID
        scanTime: getCurrentDateTime(),
      });
      scanForm.scanStatus = "已扫码";
      scanForm.scanRemark = scanForm.scanRemark || "无";
      proxy.$modal.msgSuccess("扫码登记成功!");
      closeScanDialog();
    }
  });
};
// èŽ·å–å½“å‰æ—¥æœŸæ—¶é—´
function getCurrentDateTime() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  const seconds = String(now.getSeconds()).padStart(2, "0");
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// æ·»åŠ è¡Œç±»åæ–¹æ³•
const tableRowClassName = ({ row }) => {
  return row.isInvalid ? 'invalid-row' : '';
};
onMounted(() => {
  getList();
});
</script>
<style scoped lang="scss">
.invalid-row {
  opacity: 0.6;
  background-color: #f5f7fa;
}
  .invalid-row {
    opacity: 0.6;
    background-color: #f5f7fa;
  }
</style>
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
@@ -212,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>
@@ -235,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,
@@ -254,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);
@@ -564,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;
    }
    await delRuleFile([row.id]);
    ElMessage.success("删除成功");
    await refreshFileList();
  };
  // æ‰“开附件弹框
  const openFileDialog = async (row) => {
    recordId.value = row.id
    fileDialogVisible.value = true
  }
  // èŽ·å–è§„ç« åˆ¶åº¦åˆ—è¡¨æ•°æ®
  const getRegulationList = async () => {
src/views/collaborativeApproval/sealManagement/index.vue
@@ -87,10 +87,18 @@
        </el-form-item>
        <el-form-item label="紧急程度" prop="urgency">
          <el-radio-group v-model="sealForm.urgency">
            <el-radio label="normal">普通</el-radio>
            <el-radio label="urgent">紧急</el-radio>
            <el-radio label="very-urgent">特急</el-radio>
            <el-radio value="normal">普通</el-radio>
            <el-radio value="urgent">紧急</el-radio>
            <el-radio value="very-urgent">特急</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="附件上传">
          <AttachmentUploadFile
            v-model:fileList="sealForm.storageBlobDTOs"
            :limit="10"
            :fileSize="50"
            buttonText="点击上传附件"
          />
        </el-form-item>
      </el-form>
    </FormDialog>
@@ -119,8 +127,27 @@
          </el-descriptions-item>
          <el-descriptions-item label="申请原因" :span="2">{{ currentSealDetail.reason }}</el-descriptions-item>
        </el-descriptions>
        <!-- é™„件列表 -->
        <div v-if="currentSealDetail.storageBlobVOList?.length || currentSealDetail.storageBlobDTOs?.length" class="attachment-section">
          <div class="attachment-title">附件列表:</div>
          <el-table :data="currentSealDetail.storageBlobVOList || currentSealDetail.storageBlobDTOs" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">
                {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }}
              </template>
            </el-table-column>
            <el-table-column fixed="right" label="操作" width="150" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </FormDialog>
    <!-- æ–‡ä»¶é¢„览组件 -->
    <FilePreview ref="filePreviewRef" />
  </div>
</template>
@@ -134,6 +161,9 @@
import useUserStore from '@/store/modules/user'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue'
import FilePreview from '@/components/filePreview/index.vue'
import download from '@/plugins/download.js'
// å“åº”式数据
// ç”¨å°ç”³è¯·ç›¸å…³
@@ -143,6 +173,7 @@
const tableLoading = ref(false)
const showSealDetailDialog = ref(false)
const currentSealDetail = ref(null)
const filePreviewRef = ref(null)
const sealFormRef = ref()
const userList = ref([])
const sealForm = reactive({
@@ -152,7 +183,8 @@
  reason: '',
  approveUserId: '',
  urgency: 'normal',
  status: 'pending'
  status: 'pending',
  storageBlobDTOs: []
})
const sealRules = {
@@ -281,7 +313,8 @@
        reason: '',
        approveUserId: '',
        urgency: 'normal',
        status: 'pending'
        status: 'pending',
        storageBlobDTOs: []
      })
      }
    }).catch(err => {
@@ -301,7 +334,8 @@
    reason: '',
    approveUserId: '',
    urgency: 'normal',
    status: 'pending'
    status: 'pending',
    storageBlobDTOs: []
  })
  // æ¸…除表单验证状态
  if (sealFormRef.value) {
@@ -318,6 +352,27 @@
const viewSealDetail = (row) => {
  currentSealDetail.value = row
  showSealDetailDialog.value = true
}
// é¢„览文件
const previewFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url
  if (url && filePreviewRef.value) {
    filePreviewRef.value.open(url)
  } else {
    ElMessage.warning('文件地址无效,无法预览')
  }
}
// ä¸‹è½½æ–‡ä»¶
const downloadFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download'
    download.byUrl(url, filename)
  } else {
    ElMessage.warning('文件地址无效,无法下载')
  }
}
// å®¡æ‰¹ç”¨å°ç”³è¯·
const approveSeal = (row) => {
@@ -421,4 +476,19 @@
.ml-10 {
  margin-left: 10px;
}
.attachment-section {
  margin-top: 20px;
}
.attachment-title {
  font-size: 14px;
  color: #606266;
  margin-bottom: 10px;
  font-weight: 500;
}
.attachment-table {
  border-radius: 4px;
}
</style>
src/views/collaborativeApproval/shipmentReview/fileList.vue
@@ -29,8 +29,7 @@
  tableData.value = list
}
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
    proxy.$download.byUrl(row.url, row.originalFilename);
}
const lookFile = (row) => {
  filePreviewRef.value.open(row.url)
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"
        />
    </div>
    <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/components/formDia.vue
@@ -20,7 +20,7 @@
                                v-model="form.productName"
                                placeholder="请输入产品名称"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('productName')"
                            />
                        </el-form-item>
                    </el-col>
@@ -30,7 +30,7 @@
                                v-model="form.batchNumber"
                                placeholder="请输入产品批号"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('batchNumber')"
                            />
                        </el-form-item>
                    </el-col>
@@ -46,7 +46,7 @@
                                type="date"
                                placeholder="请选择临期日期"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('expiryDate')"
                            />
                        </el-form-item>
                    </el-col>
@@ -57,7 +57,7 @@
                                :min="0"
                                placeholder="请输入库存数量"
                                style="width: 100%"
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('stockQuantity')"
                            />
                        </el-form-item>
                    </el-col>
@@ -69,7 +69,7 @@
                                v-model="form.customerName"
                                placeholder="请输入客户名称"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('customerName')"
                            />
                        </el-form-item>
                    </el-col>
@@ -79,7 +79,7 @@
                                v-model="form.contactPhone"
                                placeholder="请输入联系电话"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('contactPhone')"
                            />
                        </el-form-item>
                    </el-col>
@@ -91,7 +91,7 @@
                                v-model="form.problemDesc"
                                placeholder="请输入问题描述"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('problemDesc')"
                                type="textarea"
                                :rows="3"
                            />
@@ -105,7 +105,7 @@
                                v-model="form.handlerId"
                                placeholder="请选择处理人"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('handlerId')"
                                style="width: 100%"
                            >
                                <el-option
@@ -127,7 +127,7 @@
                                type="date"
                                placeholder="请选择处理日期"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('handleDate')"
                            />
                        </el-form-item>
                    </el-col>
@@ -139,7 +139,7 @@
                                v-model="form.handleResult"
                                placeholder="请输入处理结果"
                                clearable
                                :disabled="operationType === 'view'"
                                :disabled="isFieldDisabled('handleResult')"
                                type="textarea"
                                :rows="3"
                            />
@@ -175,6 +175,8 @@
            return '新增临期售后';
        case 'edit':
            return '编辑临期售后';
        case 'handle':
            return '处理临期售后';
        case 'view':
            return '查看临期售后';
        default:
@@ -212,6 +214,13 @@
})
const { form, rules } = toRefs(data);
const userList = ref([])
const handleEditableFields = ["handlerId", "handleDate", "handleResult"];
const isFieldDisabled = (field) => {
    if (operationType.value === "view") return true;
    if (operationType.value === "handle") return !handleEditableFields.includes(field);
    return false;
};
// æ‰“开弹框
const openDialog = (type, row) => {
@@ -242,7 +251,7 @@
    } else {
        // ç¼–辑或查看时填充数据
        form.value = { ...row };
        if (type === 'edit' && !form.value.handlerId) {
        if (type === 'handle' && !form.value.handlerId) {
            form.value.handlerId = userStore.id;
            form.value.handleDate = getCurrentDate();
        }
@@ -250,36 +259,49 @@
}
const submitForm = () => {
    if (operationType.value === "handle") {
        if (!form.value.handlerId || !form.value.handleDate || !form.value.handleResult) {
            proxy.$modal.msgWarning("请填写处理人、处理日期和处理结果");
            return;
        }
        handleSubmit();
        return;
    }
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
            const submitData = {
                id: form.value.id,
                productName: form.value.productName,
                batchNumber: form.value.batchNumber,
                expireDate: form.value.expiryDate,
                stockQuantity: form.value.stockQuantity,
                customerName: form.value.customerName,
                contactPhone: form.value.contactPhone,
                disRes: form.value.problemDesc,
                status: form.value.status,
                disposeUserId: form.value.handlerId,
                disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName,
                disposeResult: form.value.handleResult,
                disDate: form.value.handleDate
            };
            const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate;
            apiCall(submitData).then(() => {
                proxy.$modal.msgSuccess(operationType.value === 'add' ? "新增成功" : "更新成功");
                closeDia();
            }).catch(error => {
                console.error('提交数据失败:', error);
                proxy.$modal.msgError('提交数据失败,请稍后重试');
            });
            handleSubmit();
        }
    });
}
const handleSubmit = () => {
    const submitData = {
        id: form.value.id,
        productName: form.value.productName,
        batchNumber: form.value.batchNumber,
        expireDate: form.value.expiryDate,
        stockQuantity: form.value.stockQuantity,
        customerName: form.value.customerName,
        contactPhone: form.value.contactPhone,
        disRes: form.value.problemDesc,
        status: operationType.value === "handle" ? 2 : form.value.status,
        disposeUserId: form.value.handlerId,
        disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName,
        disposeResult: form.value.handleResult,
        disDate: form.value.handleDate
    };
    const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate;
    apiCall(submitData).then(() => {
        const successText = operationType.value === "add" ? "新增成功" : operationType.value === "handle" ? "处理成功" : "更新成功";
        proxy.$modal.msgSuccess(successText);
        closeDia();
    }).catch(error => {
        console.error('提交数据失败:', error);
        proxy.$modal.msgError('提交数据失败,请稍后重试');
    });
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
    proxy.resetForm("formRef");
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
@@ -39,7 +39,7 @@
                <el-button type="danger" @click="handleDelete">删除</el-button>
            </div>
        </div>
        <div class="table_list">
            <PIMTable
                rowKey="id"
@@ -60,7 +60,7 @@
                <template #operation="{ row }">
                    <el-button type="primary" link @click="openForm('view', row)">查看</el-button>
                    <el-button type="primary" link @click="openForm('edit', row)" v-if="row.status === 1">编辑</el-button>
                    <el-button type="primary" link @click="openForm('handle', row)" v-if="row.status === 1">处理</el-button>
                </template>
            </PIMTable>
        </div>
@@ -201,7 +201,7 @@
        current: page.value.current,
        size: page.value.size
    };
    expiryAfterSalesListPage(queryParams).then(res => {
        // æ˜ å°„后端返回数据到前端表格
        tableData.value = res.data.records.map(item => ({
src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -1,489 +1,507 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        title="新增售后单"
        width="90%"
        @close="closeDia"
    >
    <el-dialog v-model="dialogFormVisible"
               title="新增售后单"
               width="90%"
               @close="closeDia">
      <div>
        <span class="descriptions">基础资料</span>
        <el-form
            :model="form"
            label-width="140px"
            label-position="top"
            :rules="rules"
            ref="formRef"
        >
        <el-form :model="form"
                 label-width="140px"
                 label-position="top"
                 :rules="rules"
                 ref="formRef">
          <el-row :gutter="30">
            <el-col :span="4">
              <el-form-item label="客户名称:" prop="customerName">
                <el-select
                    v-model="form.customerName"
                    filterable
                    @change="customerNameChange"
                >
                  <el-option
                      v-for="item in customerNameOptions"
                      :key="item.value"
                      :label="item.label"
                      :value="item.value"
                  />
              <el-form-item label="客户名称:"
                            prop="customerName">
                <el-select v-model="form.customerName"
                           filterable
                           @change="customerNameChange">
                  <el-option v-for="item in customerNameOptions"
                             :key="item.value"
                             :label="item.label"
                             :value="item.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="售后类型:" prop="serviceType">
                <el-select
                    v-model="form.serviceType"
                    filterable
                >
                  <el-option
                      v-for="dict in serviceTypeOptions"
                      :key="dict.value"
                      :label="dict.label"
                      :value="dict.value"
                  />
              <el-form-item label="售后类型:"
                            prop="serviceType">
                <el-select v-model="form.serviceType"
                           filterable>
                  <el-option v-for="dict in serviceTypeOptions"
                             :key="dict.value"
                             :label="dict.label"
                             :value="dict.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="关联销售单号:" prop="salesContractNo">
                <el-select
                    v-model="form.salesContractNo"
                    @change="associatedSalesOrderNumberChange"
                    filterable
                >
                  <el-option
                      v-for="item in associatedSalesOrderNumberOptions"
                      :key="item.value"
                      :label="item.label"
                      :value="item.value"
                  />
              <el-form-item label="关联销售单号:"
                            prop="salesContractNo">
                <el-select v-model="form.salesContractNo"
                           @change="associatedSalesOrderNumberChange"
                           filterable>
                  <el-option v-for="item in associatedSalesOrderNumberOptions"
                             :key="item.value"
                             :label="item.label"
                             :value="item.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="紧急程度:" prop="urgency">
                <el-select
                    v-model="form.urgency"
                    filterable
                >
                  <el-option
                      v-for="dict in urgencyOptions"
                      :key="dict.value"
                      :label="dict.label"
                      :value="dict.value"
                  />
              <el-form-item label="紧急程度:"
                            prop="urgency">
                <el-select v-model="form.urgency"
                           filterable>
                  <el-option v-for="dict in urgencyOptions"
                             :key="dict.value"
                             :label="dict.label"
                             :value="dict.value" />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="问题描述:" prop="proDesc">
                <el-input
                    v-model="form.proDesc"
                    placeholder="请输入问题描述"
                />
              <el-form-item label="问题描述:"
                            prop="proDesc">
                <el-input v-model="form.proDesc"
                          placeholder="请输入问题描述" />
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
        <hr>
          <div style="padding-top: 20px">
            <div style="display: flex; justify-content: space-between">
              <span class="descriptions">关联产品</span>
            <el-button
              type="primary"
              style="margin-right: 12px; margin-bottom: 10px"
              @click="isShowProductSelectDialog = true"
            >
        <div style="padding-top: 20px">
          <div style="display: flex; justify-content: space-between">
            <span class="descriptions">关联产品</span>
            <el-button type="primary"
                       style="margin-right: 12px; margin-bottom: 10px"
                       @click="isShowProductSelectDialog = true">
              é€‰æ‹©äº§å“
            </el-button>
            </div>
            <PIMTable
                :isShowPagination="false"
                rowKey="id"
                :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) }}
                </el-tag>
              </template>
            </PIMTable>
          </div>
          <PIMTable :isShowPagination="false"
                    rowKey="id"
                    :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) }}
              </el-tag>
            </template>
          </PIMTable>
        </div>
      </div>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">确认</el-button>
                    <el-button @click="closeDia">取消</el-button>
                </div>
            </template>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©äº§å“å¼¹çª— -->
    <ProductSelectDialog
      v-model="isShowProductSelectDialog"
      :products="currentSalesOrderProducts"
      :selected-ids="currentSelectedProductIds"
      @confirm="handleSelectProducts"
    />
    <ProductSelectDialog v-model="isShowProductSelectDialog"
                         :products="currentSalesOrderProducts"
                         :selected-ids="currentSelectedProductIds"
                         @confirm="handleSelectProducts" />
  </div>
</template>
<script setup>
import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue";
import ProductSelectDialog from "./ProductSelectDialog.vue";
import useUserStore from "@/store/modules/user.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import {afterSalesServiceAdd, afterSalesServiceUpdate, getAllCustomerList, getSalesLedger } from "@/api/customerService/index.js";
import { getCurrentDate } from "@/utils/index.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const formRef = ref(null)
const customerNameOptions = ref([])
const userStore = useUserStore();
  import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue";
  import ProductSelectDialog from "./ProductSelectDialog.vue";
  import useUserStore from "@/store/modules/user.js";
  import { userListNoPageByTenantId } from "@/api/system/user.js";
  import {
    afterSalesServiceAdd,
    afterSalesServiceUpdate,
    getAllCustomerList,
    getSalesLedger,
  } from "@/api/customerService/index.js";
  import { getCurrentDate } from "@/utils/index.js";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const formRef = ref(null);
  const customerNameOptions = ref([]);
  const userStore = useUserStore();
const data = reactive({
    form: {
    topic: "",
    serviceType: "",
    urgency: "",
    salesLedgerId: null,
    productModelIds: "",
    customerId: null,
    salesContractNo: "",
    proDesc: "",
    customerName: ""
    },
    rules: {
    customerName: [{required: true, message: "请选择客户名称", trigger: "change"}],
    serviceType: [{required: true, message: "请选择售后类型", trigger: "change"}],
    urgency: [{required: true, message: "请选择紧急程度", trigger: "change"}],
        feedbackDate: [{required: true, message: "请选择", trigger: "change"}],
    }
})
// è‡ªå®šä¹‰æ ¡éªŒå‡½æ•°ï¼šåˆ¤æ–­æ˜¯å¦éœ€è¦æ ¡éªŒå”®åŽç¼–号
const { form, rules } = toRefs(data);
const userList = ref([])
const formatCurrency = (val) => {
  if (val === null || val === undefined || val === '') return '-'
  const num = Number(val)
  return Number.isFinite(num) ? num.toFixed(2) : '-'
}
const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict(
  "post_sale_waiting_list",
  "degree_of_urgency"
);
const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []);
const urgencyOptions = computed(() => degree_of_urgency?.value || []);
const getProductRowId = (row) => {
  return row?.id ?? row?.productModelId ?? row?.modelId ?? `${row?.productCategory || row?.productName || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}`
}
const normalizeProductRow = (row) => {
  return {
    ...row,
    id: getProductRowId(row),
    productCategory: row?.productCategory ?? row?.productName ?? '',
    specificationModel: row?.specificationModel ?? row?.model ?? '',
    unit: row?.unit ?? '',
    approveStatus: row?.approveStatus ?? null,
    shippingStatus: row?.shippingStatus ?? '',
    expressCompany: row?.expressCompany ?? '',
    expressNumber: row?.expressNumber ?? '',
    shippingCarNumber: row?.shippingCarNumber ?? '',
    shippingDate: row?.shippingDate ?? '',
    quantity: row?.quantity ?? 0,
    taxRate: row?.taxRate ?? 0,
    taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0,
    taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0,
    taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0,
  }
}
const tableColumn = ref([
  { label: "产品大类", prop: "productCategory" },
  { label: "尺寸", prop: "specificationModel" },
  { label: "单位", prop: "unit" },
  {
    label: "产品状态",
    prop: "approveStatus",
    width: 100,
    align: "center",
    dataType: "slot",
    slot: "approveStatus",
  },
  {
    label: "发货状态",
    align: "center",
    width: 140,
    dataType: "slot",
    slot: "shippingStatus",
  },
  { label: "快递公司", prop: "expressCompany", width: 140 },
  { label: "快递单号", prop: "expressNumber", width: 160 },
  { label: "发货车牌", prop: "shippingCarNumber", minWidth: 100, align: "center" },
  { label: "发货日期", prop: "shippingDate", minWidth: 100, align: "center" },
  { label: "数量", prop: "quantity", width: 100 },
  { label: "税率(%)", prop: "taxRate", width: 100 },
  {
    label: "含税单价(元)",
    prop: "taxInclusiveUnitPrice",
    width: 160,
    formatData: formatCurrency,
  },
  {
    label: "含税总价(元)",
    prop: "taxInclusiveTotalPrice",
    width: 160,
    formatData: formatCurrency,
  },
  {
    label: "不含税总价(元)",
    prop: "taxExclusiveTotalPrice",
    width: 160,
    formatData: formatCurrency,
  },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    operation: [
      {
        name: "删除",
        type: "text",
        clickFun: (row) => {
          tableData.value = tableData.value.filter(i => getProductRowId(i) !== getProductRowId(row))
        },
      },
    ],
  },
])
const tableData = ref([])
// é€‰æ‹©äº§å“å¼¹çª—
const isShowProductSelectDialog = ref(false)
const handleSelectProducts = (rows) => {
  if (!Array.isArray(rows)) return
  const existingIds = new Set(tableData.value.map(i => String(getProductRowId(i))))
  const mapped = rows
    .map(normalizeProductRow)
    .filter(r => !existingIds.has(String(getProductRowId(r))))
  tableData.value = tableData.value.concat(mapped)
}
const currentSelectedProductIds = computed(() => {
  return tableData.value.map(item => getProductRowId(item)).filter(item => item !== undefined && item !== null && item !== '')
})
const associatedSalesOrderNumberChange = () => {
  const opt = associatedSalesOrderNumberOptions.value.find(
    (item) => item.value === form.value.salesContractNo
  )
  tableData.value = (opt?.productData || []).map(normalizeProductRow)
  form.value.salesLedgerId = opt?.id || null
}
const associatedSalesOrderNumberOptions = ref([])
const currentSalesOrderProducts = computed(() => {
  const opt = associatedSalesOrderNumberOptions.value.find(
    (item) => item.value === form.value.salesContractNo
  )
  return (opt?.productData || []).map(normalizeProductRow)
})
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
  }).then(res => {
    if(res.code === 200){
      associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({
        label: item.salesContractNo,
        value: item.salesContractNo,
        productData:item.productData,
        id: item.id
      }))
    }
  })
}
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) => {
  if (!row) return '待发货'
  if (row.shippingDate || row.shippingCarNumber) {
    return '已发货'
  }
  const status = row.shippingStatus
  if (status === null || status === undefined || status === '') {
    return '待发货'
  }
  const map = {
    '待发货': '待发货',
    '待审核': '待审核',
    '审核中': '审核中',
    '审核拒绝': '审核拒绝',
    '审核通过': '审核通过',
    '已发货': '已发货'
  }
  return map[String(status).trim()] || '待发货'
}
const getShippingStatusType = (row) => {
  if (!row) return 'info'
  if (row.shippingDate || row.shippingCarNumber) {
    return 'success'
  }
  const status = row.shippingStatus
  if (status === null || status === undefined || status === '') {
    return 'info'
  }
  const map = {
    '待发货': 'info',
    '待审核': 'warning',
    '审核中': 'warning',
    '审核拒绝': 'danger',
    '审核通过': 'success',
    '已发货': 'success'
  }
  return map[String(status).trim()] || 'info'
}
// æ‰“开弹框
const openDialog =async (type, row) => {
  // è¯·æ±‚多个接口,获取数据
  let res = await getAllCustomerList({
    current: 1,
  size: 1000,
  total: 0,
  const data = reactive({
    form: {
      topic: "",
      serviceType: "",
      urgency: "",
      salesLedgerId: null,
      productModelIds: "",
      customerId: null,
      salesContractNo: "",
      proDesc: "",
      customerName: "",
    },
    rules: {
      customerName: [
        { required: true, message: "请选择客户名称", trigger: "change" },
      ],
      serviceType: [
        { required: true, message: "请选择售后类型", trigger: "change" },
      ],
      urgency: [{ required: true, message: "请选择紧急程度", trigger: "change" }],
      feedbackDate: [{ required: true, message: "请选择", trigger: "change" }],
    },
  });
  if(res.records){
    customerNameOptions.value = res.records.map(item => ({
      label: item.customerName,
      value: item.customerName,
      id: item.id
    }));
  }
  // è‡ªå®šä¹‰æ ¡éªŒå‡½æ•°ï¼šåˆ¤æ–­æ˜¯å¦éœ€è¦æ ¡éªŒå”®åŽç¼–号
  operationType.value = type;
  dialogFormVisible.value = true;
    form.value = {}
    proxy.resetForm("formRef");
    form.value.checkUserId = userStore.id;
    form.value.feedbackDate = getCurrentDate();
  // æ–°å¢žæ—¶æ¸…空已选关联产品
  if (type === "add") {
    tableData.value = []
  }
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    if (type === "edit") {
        form.value = {...row}
    if (form.value.customerName) {
      const res = await getSalesLedger({ customerName: form.value.customerName })
      if (res?.code === 200) {
        console.log(res)
        associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(item => ({
  const { form, rules } = toRefs(data);
  const userList = ref([]);
  const formatCurrency = val => {
    if (val === null || val === undefined || val === "") return "-";
    const num = Number(val);
    return Number.isFinite(num) ? num.toFixed(2) : "-";
  };
  const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict(
    "post_sale_waiting_list",
    "degree_of_urgency"
  );
  const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []);
  const urgencyOptions = computed(() => degree_of_urgency?.value || []);
  const getProductRowId = row => {
    return (
      row?.id ??
      row?.productModelId ??
      row?.modelId ??
      `${row?.productCategory || row?.productName || ""}-${
        row?.specificationModel || row?.model || ""
      }-${row?.unit || ""}`
    );
  };
  const normalizeProductRow = row => {
    return {
      ...row,
      id: getProductRowId(row),
      productCategory: row?.productCategory ?? row?.productName ?? "",
      specificationModel: row?.specificationModel ?? row?.model ?? "",
      unit: row?.unit ?? "",
      approveStatus: row?.approveStatus ?? null,
      shippingStatus: row?.shippingStatus ?? "",
      expressCompany: row?.expressCompany ?? "",
      expressNumber: row?.expressNumber ?? "",
      shippingCarNumber: row?.shippingCarNumber ?? "",
      shippingDate: row?.shippingDate ?? "",
      quantity: row?.quantity ?? 0,
      taxRate: row?.taxRate ?? 0,
      taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0,
      taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0,
      taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0,
      noQuantity: row?.noQuantity ?? 0,
    };
  };
  const tableColumn = ref([
    { label: "产品大类", prop: "productCategory" },
    { label: "规格型号", prop: "specificationModel" },
    { label: "单位", prop: "unit" },
    {
      label: "产品状态",
      prop: "approveStatus",
      width: 100,
      align: "center",
      dataType: "slot",
      slot: "approveStatus",
    },
    {
      label: "发货状态",
      align: "center",
      width: 140,
      dataType: "slot",
      slot: "shippingStatus",
    },
    { label: "快递公司", prop: "expressCompany", width: 140 },
    { label: "快递单号", prop: "expressNumber", width: 160 },
    {
      label: "发货车牌",
      prop: "shippingCarNumber",
      minWidth: 100,
      align: "center",
    },
    { label: "发货日期", prop: "shippingDate", minWidth: 100, align: "center" },
    { label: "数量", prop: "quantity", width: 100 },
    { label: "税率(%)", prop: "taxRate", width: 100 },
    {
      label: "含税单价(元)",
      prop: "taxInclusiveUnitPrice",
      width: 160,
      formatData: formatCurrency,
    },
    {
      label: "含税总价(元)",
      prop: "taxInclusiveTotalPrice",
      width: 160,
      formatData: formatCurrency,
    },
    {
      label: "不含税总价(元)",
      prop: "taxExclusiveTotalPrice",
      width: 160,
      formatData: formatCurrency,
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      operation: [
        {
          name: "删除",
          type: "text",
          clickFun: row => {
            tableData.value = tableData.value.filter(
              i => getProductRowId(i) !== getProductRowId(row)
            );
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  // é€‰æ‹©äº§å“å¼¹çª—
  const isShowProductSelectDialog = ref(false);
  const handleSelectProducts = rows => {
    if (!Array.isArray(rows)) return;
    const existingIds = new Set(
      tableData.value.map(i => String(getProductRowId(i)))
    );
    const mapped = rows
      .map(normalizeProductRow)
      .filter(r => !existingIds.has(String(getProductRowId(r))));
    tableData.value = tableData.value.concat(mapped);
  };
  const currentSelectedProductIds = computed(() => {
    return tableData.value
      .map(item => getProductRowId(item))
      .filter(item => item !== undefined && item !== null && item !== "");
  });
  const associatedSalesOrderNumberChange = () => {
    const opt = associatedSalesOrderNumberOptions.value.find(
      item => item.value === form.value.salesContractNo
    );
    tableData.value = (opt?.productData || []).map(normalizeProductRow);
    form.value.salesLedgerId = opt?.id || null;
  };
  const associatedSalesOrderNumberOptions = ref([]);
  const currentSalesOrderProducts = computed(() => {
    const opt = associatedSalesOrderNumberOptions.value.find(
      item => item.value === form.value.salesContractNo
    );
    return (opt?.productData || []).map(normalizeProductRow);
  });
  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,
    }).then(res => {
      if (res.code === 200) {
        associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({
          label: item.salesContractNo,
          value: item.salesContractNo,
          productData: item.productData,
          id: item.id
        }))
          id: item.id,
        }));
      }
    });
  };
  const getApproveStatusText = row => {
    if (!row) return "不足";
    if (
      row.approveStatus === 1 &&
      (!row.shippingDate || !row.shippingCarNumber)
    ) {
      return "充足";
    }
    console.log(form.value)
    }
}
const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
        if (valid) {
      // åŒ¹é…äº§å“åž‹å·IDs
      form.value.productModelIds = tableData.value.map(item => item.id).join(",")
            if (operationType.value === "add") {
                afterSalesServiceAdd(form.value).then(response => {
                    proxy.$modal.msgSuccess("新增成功")
                    closeDia()
                })
            } else {
                afterSalesServiceUpdate(form.value).then(response => {
                    proxy.$modal.msgSuccess("修改成功")
                    closeDia()
                })
            }
        }
    })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
    proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  emit('close')
};
defineExpose({
  openDialog,
});
    if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) {
      return "已出库";
    }
    return "不足";
  };
  const getApproveStatusType = row => {
    const statusText = getApproveStatusText(row);
    return statusText === "不足" ? "danger" : "success";
  };
  const getShippingStatusText = row => {
    if (!row) return "待发货";
    if (row.shippingDate || row.shippingCarNumber) {
      return "已发货";
    }
    const status = row.shippingStatus;
    if (status === null || status === undefined || status === "") {
      return "待发货";
    }
    const map = {
      å¾…发货: "待发货",
      å¾…审核: "待审核",
      å®¡æ ¸ä¸­: "审核中",
      å®¡æ ¸æ‹’绝: "审核拒绝",
      å®¡æ ¸é€šè¿‡: "审核通过",
      å·²å‘è´§: "已发货",
    };
    return map[String(status).trim()] || "待发货";
  };
  const getShippingStatusType = row => {
    if (!row) return "info";
    if (row.shippingDate || row.shippingCarNumber) {
      return "success";
    }
    const status = row.shippingStatus;
    if (status === null || status === undefined || status === "") {
      return "info";
    }
    const map = {
      å¾…发货: "info",
      å¾…审核: "warning",
      å®¡æ ¸ä¸­: "warning",
      å®¡æ ¸æ‹’绝: "danger",
      å®¡æ ¸é€šè¿‡: "success",
      å·²å‘è´§: "success",
    };
    return map[String(status).trim()] || "info";
  };
  // æ‰“开弹框
  const openDialog = async (type, row) => {
    // è¯·æ±‚多个接口,获取数据
    let res = await getAllCustomerList({
      current: 1,
      size: 1000,
      total: 0,
    });
    console.log(res, "res");
    if (res.data.records) {
      customerNameOptions.value = res.data.records.map(item => ({
        label: item.customerName,
        value: item.customerName,
        id: item.id,
      }));
    } else {
    }
    operationType.value = type;
    dialogFormVisible.value = true;
    form.value = {};
    proxy.resetForm("formRef");
    form.value.checkUserId = userStore.id;
    form.value.feedbackDate = getCurrentDate();
    // æ–°å¢žæ—¶æ¸…空已选关联产品
    if (type === "add") {
      tableData.value = [];
    }
    userListNoPageByTenantId().then(res => {
      userList.value = res.data;
    });
    if (type === "edit") {
      form.value = { ...row };
      if (form.value.customerName) {
        const res = await getSalesLedger({
          customerName: form.value.customerName,
        });
        if (res?.code === 200) {
          console.log(res);
          associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(
            item => ({
              label: item.salesContractNo,
              value: item.salesContractNo,
              productData: item.productData,
              id: item.id,
            })
          );
        }
      }
      console.log(form.value);
    }
  };
  const submitForm = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        // åŒ¹é…äº§å“åž‹å·IDs
        form.value.productModelIds = tableData.value
          .map(item => item.id)
          .join(",");
        if (operationType.value === "add") {
          afterSalesServiceAdd(form.value).then(response => {
            proxy.$modal.msgSuccess("新增成功");
            closeDia();
          });
        } else {
          afterSalesServiceUpdate(form.value).then(response => {
            proxy.$modal.msgSuccess("修改成功");
            closeDia();
          });
        }
      }
    });
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped lang="scss">
.descriptions {
  margin-bottom: 20px;
  display: inline-block;
  font-size: 1rem;
  font-weight: 600;
  padding-left: 12px;
  position: relative;
}
  .descriptions {
    margin-bottom: 20px;
    display: inline-block;
    font-size: 1rem;
    font-weight: 600;
    padding-left: 12px;
    position: relative;
  }
.descriptions::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 4px;
  height: 1rem;
  background-color: #002FA7; /* Element é»˜è®¤çº¢è‰² */
  border-radius: 2px;
}
  .descriptions::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 4px;
    height: 1rem;
    background-color: #002fa7; /* Element é»˜è®¤çº¢è‰² */
    border-radius: 2px;
  }
</style>
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
@@ -26,6 +26,21 @@
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="巡检项目" prop="inspectionProject">
              <el-input v-model="form.inspectionProject" placeholder="请输入巡检项目" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否启用" prop="isEnabled">
              <el-radio-group v-model="form.isEnabled">
                <el-radio :value="1">是</el-radio>
                <el-radio :value="0">否</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="备注" prop="remarks">
              <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" />
            </el-form-item>
@@ -118,6 +133,8 @@
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    inspectionProject: '',
    isEnabled: 1,
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
@@ -245,6 +262,8 @@
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    inspectionProject: '',
    isEnabled: 1,
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
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 urlLower = fileUrl.toLowerCase();
    if (urlLower.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/)) {
    const fileUrl = item.previewURL;
    const contentType = String(item.contentType).toLowerCase();
    // æ ¹æ® 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/inspectionManagement/index.vue
@@ -70,6 +70,11 @@
                    class="no-data">--</span>
            </div>
          </template>
          <template #isEnabled="{ row }">
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'" size="small">
              {{ row.isEnabled == 1 ? '是' : '否' }}
            </el-tag>
          </template>
        </PIMTable>
      </div>
    </el-card>
@@ -126,8 +131,16 @@
  // åˆ—配置
  const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
    { prop: "inspectionProject", label: "巡检项目", minWidth: 150 },
    { prop: "remarks", label: "备注", minWidth: 150 },
    { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
    {
      prop: "isEnabled",
      label: "是否启用",
      minWidth: 100,
      dataType: "slot",
      slot: "isEnabled"
    },
    {
      prop: "frequencyType",
      label: "频次",
@@ -227,8 +240,10 @@
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["viewFile"]);
      // å®šæ—¶ä»»åŠ¡è®°å½•ä¸å±•ç¤º"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...columns.value,
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["viewFile"];
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
@@ -298,7 +298,34 @@
const showQRCode = async (row) => {
  // ç›´æŽ¥ä½¿ç”¨URL,不要用JSON.stringify包装
  const qrContent = proxy.javaApi + '/device-info?deviceId=' + row.id;
  qrCodeUrl.value = await QRCode.toDataURL(qrContent);
  const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 200, margin: 2 });
  // åˆ›å»ºcanvas合成带名称的二维码图片
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const qrSize = 200;
  const textHeight = 30;
  const padding = 10;
  canvas.width = qrSize + padding * 2;
  canvas.height = qrSize + textHeight + padding * 2;
  // å¡«å……白色背景
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  // ç»˜åˆ¶äºŒç»´ç 
  const qrImg = new Image();
  qrImg.src = qrDataUrl;
  await new Promise((resolve) => { qrImg.onload = resolve; });
  ctx.drawImage(qrImg, padding, padding, qrSize, qrSize);
  // ç»˜åˆ¶è®¾å¤‡åç§°
  ctx.fillStyle = '#333333';
  ctx.font = 'bold 14px Arial';
  ctx.textAlign = 'center';
  ctx.fillText(row.deviceName || '', canvas.width / 2, qrSize + padding + 20);
  qrCodeUrl.value = canvas.toDataURL('image/png');
  qrRowData.value = row;
  qrDialogVisible.value = true;
};
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/components/formDia.vue
@@ -36,15 +36,6 @@
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="安装位置:" prop="instationLocation">
                            <el-input
                                v-model="form.instationLocation"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="检定单位:" prop="unit">
              <el-input
                  v-model="form.unit"
@@ -53,17 +44,17 @@
              />
                        </el-form-item>
                    </el-col>
                </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="证书编号:" prop="model">
                    <el-col :span="12">
                        <el-form-item label="证书编号:" prop="model">
              <el-input
                  v-model="form.model"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
                    </el-col>
                </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="最新鉴定日期:" prop="mostDate">
              <el-date-picker
@@ -77,8 +68,6 @@
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="有效日期(天):" prop="valid">
              <el-input
@@ -91,15 +80,6 @@
              >
              <template #append>日</template>
              </el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检定周期:" prop="cycle">
              <el-input
                  v-model="form.cycle"
                  placeholder="请输入检定周期"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
@@ -184,10 +164,8 @@
    form: {
        code: "",
    name: "",
    instationLocation: "",
    mostDate:"",
        model: "",
    cycle:"",
        validDate: "",
        nextDate: "",
        userId: "",
@@ -203,9 +181,7 @@
        nextDate: [{required: true, message: "请选择", trigger: "change"}],
        userId: [{required: true, message: "请选择", trigger: "change"}],
        recordDate: [{required: true, message: "请选择", trigger: "change"}],
    instationLocation: [{required: true, message: "请输入", trigger: "blur"}],
    mostDate: [{required: true, message: "请选择", trigger: "change"}],
    cycle: [{required: true, message: "请选择", trigger: "blur"}],
    valid: [
      {required: true, message: "请输入", trigger: "blur"},
      {
src/views/equipmentManagement/measurementEquipment/filesDia.vue
@@ -157,7 +157,7 @@
}
// ä¸‹è½½é™„ä»¶
const downLoadFile = (row) => {
  proxy.$download.name(row.url);
    proxy.$download.byUrl(row.url, row.originalFilename);
}
// åˆ é™¤
const handleDelete = () => {
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
@@ -42,6 +42,7 @@
                :tableLoading="tableLoading"
                @pagination="pagination"
        :dbRowClick="dbRowClick"
        :rowClassName="rowClassName"
            ></PIMTable>
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
@@ -89,12 +90,6 @@
    align: "center",
  },
    {
        label: "安装位置",
        prop: "instationLocation",
        width: 150,
    align:"center"
    },
    {
        label: "检定单位",
        prop: "unit",
        width: 200,
@@ -130,12 +125,6 @@
        width: 130,
    align:"center"
    },
  {
    label: "检定周期(天)",
    prop: "cycle",
    width: 130,
    align:"center"
  },
  {
    label: "状态",
    prop: "status",
@@ -193,6 +182,31 @@
const dbRowClick = (row)=>{
  rowClickData.value?.openDialog(row)
}
// è¡Œæ ·å¼ï¼šå¿«åˆ°æœŸï¼ˆ7天内)或逾期标红
const rowClassName = ({ row }) => {
  console.log('rowClassName called:', row);
  // valid æ˜¯æœ‰æ•ˆå¤©æ•°ï¼ŒmostDate æ˜¯æœ€æ–°æ£€å®šæ—¥æœŸ
  if (row.valid && row.mostDate) {
    const mostDate = new Date(row.mostDate);
    // è®¡ç®—到期日期 = æ£€å®šæ—¥æœŸ + æœ‰æ•ˆå¤©æ•°
    const validDays = parseInt(row.valid) || 0;
    const expireDate = new Date(mostDate);
    expireDate.setDate(expireDate.getDate() + validDays);
    const now = new Date();
    const diffDays = Math.ceil((expireDate - now) / (1000 * 60 * 60 * 24));
    console.log('row:', row.code, 'validDays:', validDays, 'expireDate:', expireDate, 'diffDays:', diffDays);
    // 7天内到期或已逾期都标红
    if (diffDays <= 7) {
      console.log('return warning-row');
      return 'warning-row';
    }
  } else {
    console.log('row missing valid or mostDate:', row.valid, row.mostDate);
  }
  return '';
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
@@ -294,5 +308,13 @@
</script>
<style scoped>
:deep(.el-table .warning-row) {
  background-color: #fef0f0 !important;
}
:deep(.el-table .warning-row:hover > td) {
  background-color: #f9d5d5 !important;
}
:deep(.el-table .el-table__body tr.warning-row td) {
  background-color: #fef0f0 !important;
}
</style>
src/views/equipmentManagement/operationManagement/index.vue
@@ -104,7 +104,7 @@
          align="center"
        >
          <template #default="scope">
            {{ scope.row.runtimeDuration || '-' }}
            {{ getRuntimeDurationDisplay(scope.row) }}
          </template>
        </el-table-column>
        <el-table-column
@@ -154,7 +154,8 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
import dayjs from 'dayjs'
import { ElMessage } from 'element-plus'
import {
  VideoPlay,
@@ -193,6 +194,98 @@
  return filtered
})
// è¿è¡Œä¸­æ— ç»“束时间时,运行时长需随当前时间变化,用 tick è§¦å‘模板重算
const runtimeDisplayTick = ref(0)
/** å–后端可能使用的开始/结束时间字段 */
const pickStartTime = (row) => row?.startRuntimeTime ?? row?.startTime ?? row?.start_time
const pickEndTime = (row) => row?.endRuntimeTime ?? row?.endTime ?? row?.end_time
/**
 * è§£æžæŽ¥å£/前端写入的各类时间:时间戳、ISO å­—符串、yyyy-MM-dd HH:mm:ss、Jackson æ•°ç»„ [y,M,d,h,m,s]、含中文的 toLocaleString ç­‰
 */
const parseDeviceTime = (input) => {
  if (input === null || input === undefined || input === '') return null
  if (typeof input === 'number' && !Number.isNaN(input)) {
    const d = dayjs(input)
    return d.isValid() ? d.toDate() : null
  }
  if (Array.isArray(input)) {
    const [y, mo, day, h = 0, mi = 0, se = 0] = input
    if (y == null || y === '') return null
    const d = dayjs()
        .year(Number(y))
        .month(Number(mo || 1) - 1)
        .date(Number(day || 1))
        .hour(Number(h) || 0)
        .minute(Number(mi) || 0)
        .second(Number(se) || 0)
    return d.isValid() ? d.toDate() : null
  }
  const s = String(input).trim()
  if (!s || s === '-') return null
  let d = dayjs(s)
  if (d.isValid()) return d.toDate()
  d = dayjs(s.replace(/-/g, '/'))
  if (d.isValid()) return d.toDate()
  d = dayjs(s.replace(/\//g, '-'))
  if (d.isValid()) return d.toDate()
  return null
}
const formatDurationMs = (durationMs) => {
  if (durationMs == null || Number.isNaN(durationMs) || durationMs < 0) return '-'
  const hours = Math.floor(durationMs / (1000 * 60 * 60))
  const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60))
  if (hours === 0 && minutes === 0) return '不足1分钟'
  return `${hours}小时${minutes}分钟`
}
const hasMeaningfulEnd = (endRaw) =>
    endRaw !== null &&
    endRaw !== undefined &&
    String(endRaw).trim() !== '' &&
    String(endRaw).trim() !== '-'
const formatStoredDuration = (row) => {
  const rd = row?.runtimeDuration
  if (rd === null || rd === undefined) return ''
  const t = String(rd).trim()
  return t === '' || t === '-' ? '' : String(rd)
}
/** è¿è¡Œä¸­ï¼šå§‹ç»ˆç”¨ã€Œå½“前时间 - å¼€å§‹æ—¶é—´ã€ï¼›å·²åœæ­¢ï¼šä¼˜å…ˆæŽ¥å£ runtimeDuration,否则用结束-开始;无结束可看已存时长或动态推算 */
const getRuntimeDurationDisplay = (row) => {
  void runtimeDisplayTick.value
  const start = parseDeviceTime(pickStartTime(row))
  if (!start) {
    return formatStoredDuration(row) || '-'
  }
  const statusStr = String(row?.status ?? '').trim()
  const isRunning = statusStr === '运行中' || statusStr === '1'
  const endRaw = pickEndTime(row)
  const hasEnd = hasMeaningfulEnd(endRaw)
  // æ— ç»“束时间:运行中一定动态算;已停止则优先展示后端已存时长,没有再按当前时间推算
  if (!hasEnd) {
    if (isRunning) return formatDurationMs(Date.now() - start.getTime())
    const stored = formatStoredDuration(row)
    if (stored) return stored
    return formatDurationMs(Date.now() - start.getTime())
  }
  if (isRunning) {
    return formatDurationMs(Date.now() - start.getTime())
  }
  const end = parseDeviceTime(endRaw)
  const stored = formatStoredDuration(row)
  if (stored) return stored
  if (end) return formatDurationMs(end.getTime() - start.getTime())
  return '-'
}
// æ£€æŸ¥è®¾å¤‡æ˜¯å¦è¶…时未启动
const isOverdue = (device) => {
@@ -246,12 +339,11 @@
      device.endRuntimeTime = currentTime
      // è®¡ç®—运行时长
      if (device.startRuntimeTime) {
        const startTime = new Date(device.startRuntimeTime)
        const endTime = new Date(currentTime)
        const duration = endTime - startTime
        const hours = Math.floor(duration / (1000 * 60 * 60))
        const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60))
        device.runtimeDuration = `${hours}小时${minutes}分钟`
        const startTime = parseDeviceTime(device.startRuntimeTime)
        const endTime = parseDeviceTime(currentTime)
        if (startTime && endTime) {
          device.runtimeDuration = formatDurationMs(endTime.getTime() - startTime.getTime())
        }
      }
    }
    const params = {
@@ -297,9 +389,31 @@
// ç»„件挂载时初始化数据
const POLL_MS = 60 * 1000
const RUNTIME_TICK_MS = 30 * 1000
let listPollTimer = null
let runtimeTickTimer = null
// ç»„件挂载时拉取数据,并每分钟刷新一次列表;运行中时长每 30 ç§’刷新显示
onMounted(() => {
  getList()
  listPollTimer = setInterval(() => {
    getList()
  }, POLL_MS)
  runtimeTickTimer = setInterval(() => {
    runtimeDisplayTick.value++
  }, RUNTIME_TICK_MS)
})
onUnmounted(() => {
  if (listPollTimer != null) {
    clearInterval(listPollTimer)
    listPollTimer = null
  }
  if (runtimeTickTimer != null) {
    clearInterval(runtimeTickTimer)
    runtimeTickTimer = null
  }
})
</script>
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
<template>
  <FormDialog
    v-model="visible"
    title="验收审批"
    width="500px"
    @confirm="submitForm"
    @cancel="handleCancel"
    @close="handleCancel"
  >
    <el-form :model="form" :rules="rules" label-width="100px">
      <el-form-item label="验收人" prop="acceptanceName">
        <el-select
          v-model="form.acceptanceName"
          placeholder="请选择验收人"
          filterable
          style="width: 100%"
        >
          <el-option
            v-for="item in userList"
            :key="item.userId"
            :label="item.nickName"
            :value="item.nickName"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="验收时间" prop="acceptanceTime">
        <el-date-picker
          v-model="form.acceptanceTime"
          type="datetime"
          placeholder="请选择验收时间"
          format="YYYY-MM-DD HH:mm:ss"
          value-format="YYYY-MM-DD HH:mm:ss"
          style="width: 100%"
        />
      </el-form-item>
      <el-form-item label="验收备注" prop="acceptanceRemark">
        <el-input
          v-model="form.acceptanceRemark"
          type="textarea"
          :rows="3"
          placeholder="请输入验收备注"
        />
      </el-form-item>
    </el-form>
  </FormDialog>
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { repairAcceptance } from "@/api/equipmentManagement/repair";
import dayjs from "dayjs";
defineOptions({
  name: "验收审批弹窗",
});
const emits = defineEmits(["ok"]);
const visible = ref(false);
const loading = ref(false);
const repairId = ref(null);
const userList = ref([]);
const form = reactive({
  acceptanceName: undefined,
  acceptanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
  acceptanceRemark: undefined,
});
const rules = {
  acceptanceName: [
    { required: true, message: "请选择验收人", trigger: "change" },
  ],
  acceptanceTime: [
    { required: true, message: "请选择验收时间", trigger: "change" },
  ],
  acceptanceRemark: [
    { required: true, message: "请输入验收备注", trigger: "blur" },
  ],
};
// åŠ è½½ç”¨æˆ·åˆ—è¡¨
const loadUserList = async () => {
  const { data } = await userListNoPageByTenantId();
  userList.value = data;
};
// æ‰“开弹窗
const open = async (row) => {
  repairId.value = row.id;
  visible.value = true;
  // é‡ç½®è¡¨å•
  form.acceptanceName = undefined;
  form.acceptanceTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
  form.acceptanceRemark = undefined;
  await loadUserList();
};
// æäº¤è¡¨å•
const submitForm = async () => {
  if (!form.acceptanceName) {
    ElMessage.warning("请选择验收人");
    return;
  }
  if (!form.acceptanceTime) {
    ElMessage.warning("请选择验收时间");
    return;
  }
  if (!form.acceptanceRemark) {
    ElMessage.warning("请输入验收备注");
    return;
  }
  loading.value = true;
  try {
    const { code } = await repairAcceptance({
      id: repairId.value,
      acceptanceName: form.acceptanceName,
      acceptanceTime: form.acceptanceTime,
      acceptanceRemark: form.acceptanceRemark,
    });
    if (code === 200) {
      ElMessage.success("验收通过");
      visible.value = false;
      emits("ok");
    }
  } finally {
    loading.value = false;
  }
};
const handleCancel = () => {
  visible.value = false;
};
defineExpose({
  open,
});
</script>
<style lang="scss" scoped></style>
src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -49,19 +49,44 @@
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="类目">
            <el-input v-model="form.machineryCategory" placeholder="请输入类目" />
          <el-form-item label="报修报修项目">
            <el-input v-model="form.machineryCategory" placeholder="请输入报修报修项目" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="维修人">
            <el-input v-model="form.maintenanceName" placeholder="请输入维修人姓名" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="id">
        <el-col :span="12">
          <el-form-item label="报修状态">
            <el-select v-model="form.status">
            <el-select v-model="form.status" disabled>
              <el-option label="待维修" :value="0"></el-option>
              <el-option label="完结" :value="1"></el-option>
              <el-option label="已验收" :value="1"></el-option>
              <el-option label="失败" :value="2"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <!-- éªŒæ”¶ä¿¡æ¯å±•示 -->
      <el-row v-if="id && form.status === 1">
        <el-col :span="12">
          <el-form-item label="验收人">
            <el-input v-model="form.acceptanceName" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="验收时间">
            <el-input v-model="form.acceptanceTime" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="验收备注">
            <el-input v-model="form.acceptanceRemark" type="textarea" :rows="2" disabled />
          </el-form-item>
        </el-col>
      </el-row>
@@ -77,12 +102,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,
@@ -106,6 +139,7 @@
const userStore = useUserStore();
const deviceOptions = ref([]);
const fileList = ref([]);
const loadDeviceName = async () => {
  const { data } = await getDeviceLedger();
@@ -121,6 +155,8 @@
  remark: undefined, // æ•…障现象
  status: 0, // æŠ¥ä¿®çŠ¶æ€
  machineryCategory: undefined,
  storageBlobDTOs: [],
  maintenanceName: undefined, // ç»´ä¿®äºº
});
const setDeviceModel = (deviceId) => {
@@ -137,6 +173,11 @@
  form.remark = data.remark;
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.storageBlobDTOs = data.storageBlobVOs || [];
  form.maintenanceName = data.maintenanceName;
  form.acceptanceName = data.acceptanceName;
  form.acceptanceTime = data.acceptanceTime;
  form.acceptanceRemark = data.acceptanceRemark;
};
const sendForm = async () => {
@@ -168,6 +209,7 @@
const openAdd = async () => {
  id.value = undefined;
  visible.value = true;
  fileList.value = [];
  await nextTick();
  await loadDeviceName();
};
src/views/equipmentManagement/repair/index.vue
@@ -100,13 +100,14 @@
        <template #statusRef="{ row }">
          <el-tag v-if="row.status === 2" type="danger">失败</el-tag>
          <el-tag v-if="row.status === 1" type="success">完结</el-tag>
          <el-tag v-if="row.status === 3" type="info">待验收</el-tag>
          <el-tag v-if="row.status === 0" type="warning">待维修</el-tag>
        </template>
        <template #operation="{ row }">
          <el-button
            type="primary"
            link
            :disabled="row.status === 1"
            :disabled="row.status === 1 || row.status === 3"
            @click="editRepair(row.id)"
          >
            ç¼–辑
@@ -114,35 +115,54 @@
          <el-button
            type="success"
            link
            :disabled="row.status === 1"
            :disabled="row.status !== 0"
            @click="addMaintain(row)"
          >
            ç»´ä¿®
          </el-button>
          <el-button
            type="warning"
            link
            :disabled="row.status !== 3"
            @click="openAcceptance(row)"
          >
            éªŒæ”¶
          </el-button>
          <el-button
            type="danger"
            link
            :disabled="row.status === 1"
            :disabled="row.status === 1 || row.status === 3"
            @click="delRepairByIds(row.id)"
          >
            åˆ é™¤
          </el-button>
          <el-button
              type="primary"
              link
              @click="openFileDialog(row)"
          >
            é™„ä»¶
          </el-button>
        </template>
      </PIMTable>
    </div>
    <RepairModal ref="repairModalRef" @ok="getTableData"/>
    <MaintainModal ref="maintainModalRef" @ok="getTableData"/>
    <AcceptanceModal ref="acceptanceModalRef" @ok="getTableData"/>
    <FileList v-if="fileDialogVisible"  v-model:visible="fileDialogVisible" :record-type="'device_repair'" :record-id="recordId"  />
  </div>
</template>
<script setup>
import { onMounted, getCurrentInstance, computed } 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";
import AcceptanceModal from "./Modal/AcceptanceModal.vue";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
defineOptions({
  name: "设备报修",
@@ -153,6 +173,7 @@
// æ¨¡æ€æ¡†å®žä¾‹
const repairModalRef = ref();
const maintainModalRef = ref();
const acceptanceModalRef = ref();
// è¡¨æ ¼å¤šé€‰æ¡†é€‰ä¸­é¡¹
const multipleList = ref([]);
@@ -188,7 +209,7 @@
        prop: "deviceModel",
      },
      {
        label: "类目",
        label: "报修项目",
        align: "center",
        prop: "machineryCategory",
      },
@@ -225,6 +246,17 @@
        formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD") : ""),
      },
      {
        label: "验收人",
        align: "center",
        prop: "acceptanceName",
      },
      {
        label: "验收时间",
        align: "center",
        prop: "acceptanceTime",
        formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : ""),
      },
      {
        label: "状态",
        align: "center",
        prop: "status",
@@ -258,6 +290,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;
@@ -283,6 +324,11 @@
  maintainModalRef.value.open(row.id, row);
};
// æ‰“开验收弹窗
const openAcceptance = (row) => {
  acceptanceModalRef.value.open(row);
};
const changePage = ({page, limit}) => {
  pagination.currentPage = page;
  pagination.pageSize = limit;
src/views/equipmentManagement/spareParts/index.vue
@@ -88,8 +88,8 @@
          </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>
src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -32,10 +32,10 @@
          disabled
        />
      </el-form-item>
      <el-form-item label="类目">
      <el-form-item label="保养项目">
        <el-input
            v-model="form.machineryCategory"
            placeholder="请输入类目"
            placeholder="请输入保养项目"
        />
      </el-form-item>
      <el-form-item label="录入人">
@@ -62,6 +62,13 @@
          <el-option label="失败" :value="2"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="保养人">
        <el-input
          v-model="form.maintenancePerson"
          placeholder="请输入保养人姓名"
          clearable
        />
      </el-form-item>
      <el-form-item label="计划保养日期">
        <el-date-picker
          style="width: 100%"
@@ -73,6 +80,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>
@@ -90,6 +104,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: "设备保养新增计划",
@@ -115,6 +130,8 @@
  createUser: undefined, // å½•入人
  status: 0, //保修状态
  machineryCategory: undefined,
  storageBlobDTOs: [],
  maintenancePerson: undefined, // ä¿å…»äºº
});
const setDeviceModel = (deviceId) => {
@@ -133,9 +150,13 @@
  form.createUser = Number(data.createUser);
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
    "YYYY-MM-DD HH:mm:ss"
  );
  form.maintenancePerson = data.maintenancePerson;
  if (data.maintenancePlanTime) {
    form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
      "YYYY-MM-DD HH:mm:ss"
    );
  }
  form.storageBlobDTOs = data.storageBlobVOs || [];
};
// ç”¨æˆ·åˆ—表
src/views/equipmentManagement/upkeep/Form/formDia.vue
@@ -67,6 +67,28 @@
            </el-row>
            <el-row>
                <el-col :span="12">
                    <el-form-item label="保养项目" prop="machineryCategory">
                        <el-input
                            v-model.trim="form.machineryCategory"
                            placeholder="请输入保养项目"
                            maxlength="100"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="保养人" prop="maintenancePerson">
                        <el-input
                            v-model.trim="form.maintenancePerson"
                            placeholder="请输入保养人姓名"
                            maxlength="100"
                            clearable
                        />
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="12">
                    <el-form-item label="任务频率" prop="frequencyType">
                        <el-select v-model="form.frequencyType" placeholder="请选择" clearable>
                            <el-option label="每日" value="DAILY"/>
@@ -154,18 +176,21 @@
        taskName: undefined,
        // å½•入人:单选一个用户 id
        inspector: undefined,
        machineryCategory: "",
        remarks: '',
        frequencyType: '',
        frequencyDetail: '',
        week: '',
        time: '',
        deviceModel: undefined, // å°ºå¯¸
        registrationDate: ''
        deviceModel: undefined, // è§„格型号
        registrationDate: '',
        maintenancePerson: '' // ä¿å…»äºº
    },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请选择录入人", trigger: "blur" },],
        registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }]
        registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }],
        machineryCategory: [{ required: true, message: "请输入保养项目", trigger: "blur" }]
    }
})
const { form, rules } = toRefs(data)
@@ -238,14 +263,15 @@
        taskId: undefined,
        taskName: undefined,
        inspector: undefined,
        inspector: undefined,
        machineryCategory: "",
        remarks: '',
        frequencyType: '',
        frequencyDetail: '',
        week: '',
        time: '',
        deviceModel: undefined,
        registrationDate: ''
        registrationDate: '',
        maintenancePerson: ''
    }
}
src/views/equipmentManagement/upkeep/index.vue
@@ -1,699 +1,644 @@
<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"
                  style="width: 240px"
                  placeholder="请输入任务名称"
                  clearable
                  :prefix-icon="Search"
                  @change="getScheduledTableData"
              />
              <el-input v-model="scheduledFilters.taskName"
                        style="width: 240px"
                        placeholder="请输入任务名称"
                        clearable
                        :prefix-icon="Search"
                        @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"
                icon="Delete"
                :disabled="scheduledMultipleList.length <= 0"
                @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))"
              >
              <el-button type="danger"
                         icon="Delete"
                         :disabled="scheduledMultipleList.length <= 0"
                         @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))">
                æ‰¹é‡åˆ é™¤
              </el-button>
            </div>
          </div>
          <PIMTable
            rowKey="id"
            isSelection
            :column="scheduledColumns"
            :tableData="scheduledDataList"
            :page="{
          <PIMTable rowKey="id"
                    isSelection
                    :column="scheduledColumns"
                    :tableData="scheduledDataList"
                    :page="{
              current: scheduledPagination.currentPage,
              size: scheduledPagination.pageSize,
              total: scheduledPagination.total,
            }"
            @selection-change="handleScheduledSelectionChange"
            @pagination="changeScheduledPage"
          >
                    @selection-change="handleScheduledSelectionChange"
                    @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"
                link
                @click="editScheduledTask(row)"
              >
              <el-button type="primary"
                         link
                         @click="editScheduledTask(row)">
                ç¼–辑
              </el-button>
              <el-button
                type="danger"
                link
                @click="delScheduledTaskByIds(row.id)"
              >
              <el-button type="danger"
                         link
                         @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"
                  style="width: 240px"
                  placeholder="请输入设备名称"
                  clearable
                  :prefix-icon="Search"
                  @change="getTableData"
              />
              <el-input v-model="filters.deviceName"
                        style="width: 240px"
                        placeholder="请输入设备名称"
                        clearable
                        :prefix-icon="Search"
                        @change="getTableData" />
            </el-form-item>
            <el-form-item label="计划保养日期">
              <el-date-picker
                  v-model="filters.maintenancePlanTime"
                  type="date"
                  placeholder="请选择计划保养日期"
                  size="default"
                  @change="(date) => handleDateChange(date,2)"
              />
              <el-date-picker v-model="filters.maintenancePlanTime"
                              type="date"
                              placeholder="请选择计划保养日期"
                              size="default"
                              @change="(date) => handleDateChange(date,2)" />
            </el-form-item>
            <el-form-item label="实际保养日期">
              <el-date-picker
                  v-model="filters.maintenanceActuallyTime"
                  type="date"
                  placeholder="请选择实际保养日期"
                  size="default"
                  @change="(date) => handleDateChange(date,1)"
              />
              <el-date-picker v-model="filters.maintenanceActuallyTime"
                              type="date"
                              placeholder="请选择实际保养日期"
                              size="default"
                              @change="(date) => handleDateChange(date,1)" />
            </el-form-item>
            <el-form-item label="实际保养人">
              <el-input
                  v-model="filters.maintenanceActuallyName"
                  style="width: 240px"
                  placeholder="请输入实际保养人"
                  clearable
                  :prefix-icon="Search"
                  @change="getTableData"
              />
              <el-input v-model="filters.maintenanceActuallyName"
                        style="width: 240px"
                        placeholder="请输入实际保养人"
                        clearable
                        :prefix-icon="Search"
                        @change="getTableData" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="getTableData">搜索</el-button>
              <el-button 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"
                icon="Delete"
                :disabled="multipleList.length <= 0 || hasFinishedStatus"
                @click="delRepairByIds(multipleList.map((item) => item.id))"
              >
              <el-button type="danger"
                         icon="Delete"
                         :disabled="multipleList.length <= 0 || hasFinishedStatus"
                         @click="delRepairByIds(multipleList.map((item) => item.id))">
                æ‰¹é‡åˆ é™¤
              </el-button>
            </div>
          </div>
         <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
          <PIMTable rowKey="id"
                    isSelection
                    :column="columns"
                    :tableData="dataList"
                    :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @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>
        </template>
        <template #operation="{ row }">
          <!-- è¿™ä¸ªåŠŸèƒ½è·Ÿæ–°å¢žä¿å…»åŠŸèƒ½ä¸€æ¨¡ä¸€æ ·ï¼Œæœ‰å•¥æ„ä¹‰ï¼Ÿ -->
          <!-- <el-button
                    @selection-change="handleSelectionChange"
                    @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>
            </template>
            <template #operation="{ row }">
              <!-- è¿™ä¸ªåŠŸèƒ½è·Ÿæ–°å¢žä¿å…»åŠŸèƒ½ä¸€æ¨¡ä¸€æ ·ï¼Œæœ‰å•¥æ„ä¹‰ï¼Ÿ -->
              <!-- <el-button
              type="primary"
              text
              @click="addMaintain(row)"
          >
            æ–°å¢žä¿å…»
          </el-button> -->
          <el-button
            type="primary"
            link
            :disabled="row.status === 1"
            @click="editPlan(row.id)"
          >
            ç¼–辑
          </el-button>
          <el-button
            type="success"
            link
            :disabled="row.status === 1"
            @click="addMaintain(row)"
          >
            ä¿å…»
          </el-button>
          <el-button
            type="danger"
            link
            :disabled="row.status === 1"
            @click="delRepairByIds(row.id)"
          >
            åˆ é™¤
          </el-button>
          <el-button
            type="primary"
            link
            @click="openFileDialog(row)"
          >
            é™„ä»¶
          </el-button>
        </template>
      </PIMTable>
              <el-button type="primary"
                         link
                         :disabled="row.status === 1"
                         @click="editPlan(row.id)">
                ç¼–辑
              </el-button>
              <el-button type="success"
                         link
                         :disabled="row.status === 1"
                         @click="addMaintain(row)">
                ä¿å…»
              </el-button>
              <el-button type="danger"
                         link
                         :disabled="row.status === 1"
                         @click="delRepairByIds(row.id)">
                åˆ é™¤
              </el-button>
              <el-button type="primary"
                         link
                         @click="openFileDialog(row)">
                é™„ä»¶
              </el-button>
            </template>
          </PIMTable>
        </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 {
  getUpkeepPage,
  delUpkeep,
  deviceMaintenanceTaskList,
  deviceMaintenanceTaskDel,
} from '@/api/equipmentManagement/upkeep'
import {
  listMaintenanceTaskFiles,
  addMaintenanceTaskFile,
  delMaintenanceTaskFile,
} from '@/api/equipmentManagement/maintenanceTaskFile'
import dayjs from 'dayjs'
  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 dayjs from "dayjs";
const { proxy } = getCurrentInstance()
  const { proxy } = getCurrentInstance();
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
// Tab相关
const activeTab = ref('scheduled')
  // Tab相关
  const activeTab = ref("scheduled");
// è®¡åˆ’弹窗控制器
const planModalRef = ref()
// ä¿å…»å¼¹çª—控制器
const maintainModalRef = ref()
// å®šæ—¶ä»»åŠ¡å¼¹çª—æŽ§åˆ¶å™¨
const formDiaRef = ref()
// é™„件弹窗
const fileListDialogRef = ref(null)
const fileDialogVisible = ref(false)
const currentMaintenanceTaskId = ref(null)
  // è®¡åˆ’弹窗控制器
  const planModalRef = ref();
  // ä¿å…»å¼¹çª—控制器
  const maintainModalRef = ref();
  // å®šæ—¶ä»»åŠ¡å¼¹çª—æŽ§åˆ¶å™¨
  const formDiaRef = ref();
  // é™„件弹窗
  const fileListDialogRef = ref(null);
  const fileDialogVisible = ref(false);
  const currentMaintenanceTaskId = ref(null);
// ä»»åŠ¡è®°å½•tab(原设备保养页面)相关变量
const filters = reactive({
  deviceName: '',
  maintenancePlanTime: '',
  maintenanceActuallyTime: '',
  maintenanceActuallyName: '',
})
  // ä»»åŠ¡è®°å½•tab(原设备保养页面)相关变量
  const filters = reactive({
    deviceName: "",
    maintenancePlanTime: "",
    maintenanceActuallyTime: "",
    maintenanceActuallyName: "",
  });
const dataList = ref([])
const pagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 0,
})
const multipleList = ref([])
  const dataList = ref([]);
  const pagination = ref({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const multipleList = ref([]);
// å®šæ—¶ä»»åŠ¡ç®¡ç†tab相关变量
const scheduledFilters = reactive({
  taskName: '',
  status: '',
})
  // å®šæ—¶ä»»åŠ¡ç®¡ç†tab相关变量
  const scheduledFilters = reactive({
    taskName: "",
    status: "",
  });
const scheduledDataList = ref([])
const scheduledPagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
})
const scheduledMultipleList = ref([])
  const scheduledDataList = ref([]);
  const scheduledPagination = reactive({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const scheduledMultipleList = ref([]);
// å®šæ—¶ä»»åŠ¡ç®¡ç†è¡¨æ ¼åˆ—é…ç½®
const scheduledColumns = ref([
    { prop: "taskName", label: "设备名称"},
    {
        label: "尺寸",
        prop: "deviceModel",
    },
    {
        prop: "frequencyType",
        label: "频次",
        minWidth: 150,
        // PIMTable ä½¿ç”¨çš„æ˜¯ formatData,而不是 Element-Plus çš„ formatter
        formatData: (cell) => ({
            DAILY: "每日",
            WEEKLY: "每周",
            MONTHLY: "每月",
            QUARTERLY: "季度"
        }[cell] || "")
    },
    {
        prop: "frequencyDetail",
        label: "开始日期与时间",
        minWidth: 150,
        // åŒæ ·æ”¹ç”¨ formatData,PIMTable å†…部会把单元格值传进来
        formatData: (cell) => {
            if (typeof cell !== 'string') return '';
            let val = cell;
            const replacements = {
                MON: '周一',
                TUE: '周二',
                WED: '周三',
                THU: '周四',
                FRI: '周五',
                SAT: '周六',
                SUN: '周日'
            };
            // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
            return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => replacements[match]);
        }
    },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "registrationDate", label: "登记日期", minWidth: 100 },
    {
        fixed: "right",
        label: "操作",
        dataType: "slot",
        slot: "operation",
        align: "center",
        width: "200px",
    },
])
  // å®šæ—¶ä»»åŠ¡ç®¡ç†è¡¨æ ¼åˆ—é…ç½®
  const scheduledColumns = ref([
    { prop: "taskName", label: "设备名称" },
    {
      label: "规格型号",
      prop: "deviceModel",
    },
    {
      label: "保养项目",
      prop: "machineryCategory",
      minWidth: 120,
      formatData: cell => cell || "--",
    },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 150,
      // PIMTable ä½¿ç”¨çš„æ˜¯ formatData,而不是 Element-Plus çš„ formatter
      formatData: cell =>
        ({
          DAILY: "每日",
          WEEKLY: "每周",
          MONTHLY: "每月",
          QUARTERLY: "季度",
        }[cell] || ""),
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 150,
      // åŒæ ·æ”¹ç”¨ formatData,PIMTable å†…部会把单元格值传进来
      formatData: cell => {
        if (typeof cell !== "string") return "";
        let val = cell;
        const replacements = {
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
        };
        // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      },
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "registrationDate", label: "登记日期", minWidth: 100 },
    {
      fixed: "right",
      label: "操作",
      dataType: "slot",
      slot: "operation",
      align: "center",
      width: "200px",
    },
  ]);
// ä»»åŠ¡è®°å½•è¡¨æ ¼åˆ—é…ç½®ï¼ˆåŽŸè®¾å¤‡ä¿å…»è¡¨æ ¼åˆ—ï¼‰
const columns = ref([
    {
        label: "设备名称",
        align: "center",
        prop: "deviceName",
    },
    {
        label: "尺寸",
        align: "center",
        prop: "deviceModel",
    },
    {
        label: "计划保养日期",
        align: "center",
        prop: "maintenancePlanTime",
        formatData: (cell) => dayjs(cell).format("YYYY-MM-DD"),
    },
    {
        label: "录入人",
        align: "center",
        prop: "createUserName",
    },
  {
    label: "类目",
    align: "center",
    prop: "machineryCategory",
  },
    // {
    //   label: "录入日期",
    //   align: "center",
    //   prop: "createTime",
    //   formatData: (cell) => dayjs(cell).format("YYYY-MM-DD HH:mm:ss"),
    //   width: 200,
    // },
    {
        label: "实际保养人",
        align: "center",
        prop: "maintenanceActuallyName",
    },
    {
        label: "实际保养日期",
        align: "center",
        prop: "maintenanceActuallyTime",
        formatData: (cell) =>
            cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
        label: "保养结果",
        align: "center",
        prop: "maintenanceResult",
        dataType: "slot",
        slot: "maintenanceResultRef",
    },
    {
        label: "状态",
        align: "center",
        prop: "status",
        dataType: "slot",
        slot: "statusRef",
    },
    {
        fixed: "right",
        label: "操作",
        dataType: "slot",
        slot: "operation",
        align: "center",
        width: "350px",
    },
])
  // ä»»åŠ¡è®°å½•è¡¨æ ¼åˆ—é…ç½®ï¼ˆåŽŸè®¾å¤‡ä¿å…»è¡¨æ ¼åˆ—ï¼‰
  const columns = ref([
    {
      label: "设备名称",
      align: "center",
      prop: "deviceName",
    },
    {
      label: "规格型号",
      align: "center",
      prop: "deviceModel",
    },
    {
      label: "计划保养日期",
      align: "center",
      prop: "maintenancePlanTime",
      formatData: cell => dayjs(cell).format("YYYY-MM-DD"),
    },
    {
      label: "录入人",
      align: "center",
      prop: "createUserName",
    },
    {
      label: "保养项目",
      align: "center",
      prop: "machineryCategory",
      formatData: cell => cell || "--",
    },
    // {
    //   label: "录入日期",
    //   align: "center",
    //   prop: "createTime",
    //   formatData: (cell) => dayjs(cell).format("YYYY-MM-DD HH:mm:ss"),
    //   width: 200,
    // },
    {
      label: "实际保养人",
      align: "center",
      prop: "maintenanceActuallyName",
    },
    {
      label: "实际保养日期",
      align: "center",
      prop: "maintenanceActuallyTime",
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
      label: "保养结果",
      align: "center",
      prop: "maintenanceResult",
      dataType: "slot",
      slot: "maintenanceResultRef",
    },
    {
      label: "状态",
      align: "center",
      prop: "status",
      dataType: "slot",
      slot: "statusRef",
    },
    {
      fixed: "right",
      label: "操作",
      dataType: "slot",
      slot: "operation",
      align: "center",
      width: "350px",
    },
  ]);
// Tab切换处理
const handleTabChange = (tabName) => {
  if (tabName === 'record') {
    getTableData()
  } else if (tabName === 'scheduled') {
    getScheduledTableData()
  }
}
// å®šæ—¶ä»»åŠ¡ç®¡ç†ç›¸å…³æ–¹æ³•
const getScheduledTableData = async () => {
  try {
    const params = {
      current: scheduledPagination.currentPage,
      size: scheduledPagination.pageSize,
      taskName: scheduledFilters.taskName || undefined,
      status: scheduledFilters.status || undefined,
  // Tab切换处理
  const handleTabChange = tabName => {
    if (tabName === "record") {
      getTableData();
    } else if (tabName === "scheduled") {
      getScheduledTableData();
    }
    const { code, data } = await deviceMaintenanceTaskList(params)
    if (code === 200) {
      scheduledDataList.value = data?.records || []
      scheduledPagination.total = data?.total || 0
  };
  // å®šæ—¶ä»»åŠ¡ç®¡ç†ç›¸å…³æ–¹æ³•
  const getScheduledTableData = async () => {
    try {
      const params = {
        current: scheduledPagination.currentPage,
        size: scheduledPagination.pageSize,
        taskName: scheduledFilters.taskName || undefined,
        status: scheduledFilters.status || undefined,
      };
      const { code, data } = await deviceMaintenanceTaskList(params);
      if (code === 200) {
        scheduledDataList.value = data?.records || [];
        scheduledPagination.total = data?.total || 0;
      }
    } catch (error) {
      ElMessage.error("获取定时任务列表失败");
    }
  } catch (error) {
    ElMessage.error('获取定时任务列表失败')
  }
}
  };
const resetScheduledFilters = () => {
  scheduledFilters.taskName = ''
  scheduledFilters.status = ''
  getScheduledTableData()
}
  const resetScheduledFilters = () => {
    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');
    });
}
  const addScheduledTask = () => {
    nextTick(() => {
      formDiaRef.value?.openDialog("add");
    });
  };
const editScheduledTask = (row) => {
  if (row) {
        nextTick(() => {
            formDiaRef.value?.openDialog('edit', row);
        });
  }
}
  const editScheduledTask = row => {
    if (row) {
      nextTick(() => {
        formDiaRef.value?.openDialog("edit", row);
      });
    }
  };
const delScheduledTaskByIds = async (ids) => {
  try {
    await ElMessageBox.confirm('确定删除选中的定时任务吗?', '提示', {
      type: 'warning',
  const delScheduledTaskByIds = async ids => {
    try {
      await ElMessageBox.confirm("确定删除选中的定时任务吗?", "提示", {
        type: "warning",
      });
      const payload = Array.isArray(ids) ? ids : [ids];
      await deviceMaintenanceTaskDel(payload);
      ElMessage.success("删除定时任务成功");
      getScheduledTableData();
    } catch (error) {
      // ç”¨æˆ·å–消删除
    }
  };
  const handleScheduledOut = () => {
    ElMessage.info("导出定时任务功能待实现");
  };
  // ä»»åŠ¡è®°å½•ç›¸å…³æ–¹æ³•ï¼ˆåŽŸè®¾å¤‡ä¿å…»é¡µé¢æ–¹æ³•ï¼‰
  const getTableData = async () => {
    try {
      const params = {
        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,
        maintenanceActuallyName: filters.maintenanceActuallyName || undefined,
      };
      const { code, data } = await getUpkeepPage(params);
      if (code === 200) {
        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();
  };
  const handleSelectionChange = selection => {
    multipleList.value = selection;
  };
  // æ£€æŸ¥é€‰ä¸­çš„记录中是否有完结状态的
  const hasFinishedStatus = computed(() => {
    return multipleList.value.some(item => item.status === 1);
  });
  const changePage = page => {
    pagination.value.currentPage = page.page;
    pagination.value.pageSize = page.limit;
    getTableData();
  };
  const addMaintain = row => {
    maintainModalRef.value.open(row.id, row);
  };
  const addPlan = () => {
    planModalRef.value.openModal();
  };
  const editPlan = id => {
    planModalRef.value.openEdit(id);
  };
  const delRepairByIds = async ids => {
    // æ£€æŸ¥æ˜¯å¦æœ‰å®Œç»“状态的记录
    const hasFinished = multipleList.value.some(item => item.status === 1);
    if (hasFinished) {
      ElMessage.warning("不能删除状态为完结的记录");
      return;
    }
    try {
      await ElMessageBox.confirm("确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      });
      const { code } = await delUpkeep(ids);
      if (code === 200) {
        ElMessage.success("删除成功");
        getTableData();
      }
    } catch (error) {
      // ç”¨æˆ·å–消删除
    }
  };
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
    const payload = Array.isArray(ids) ? ids : [ids]
    await deviceMaintenanceTaskDel(payload)
    ElMessage.success('删除定时任务成功')
    getScheduledTableData()
  } catch (error) {
    // ç”¨æˆ·å–消删除
  }
}
      .then(() => {
        proxy.download("/device/maintenance/export", {}, "设备保养.xlsx");
      })
      .catch(() => {
        ElMessage.info("已取消");
      });
  };
const handleScheduledOut = () => {
  ElMessage.info('导出定时任务功能待实现')
}
// ä»»åŠ¡è®°å½•ç›¸å…³æ–¹æ³•ï¼ˆåŽŸè®¾å¤‡ä¿å…»é¡µé¢æ–¹æ³•ï¼‰
const getTableData = async () => {
  try {
    const params = {
      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,
      maintenanceActuallyName: filters.maintenanceActuallyName || undefined,
  const handleDateChange = (date, type) => {
    if (type === 1) {
      filters.maintenanceActuallyTime = date
        ? dayjs(date).format("YYYY-MM-DD")
        : "";
    } else {
      filters.maintenancePlanTime = date ? dayjs(date).format("YYYY-MM-DD") : "";
    }
    getTableData();
  };
    const { code, data } = await getUpkeepPage(params)
    if (code === 200) {
      dataList.value = data.records
      pagination.value.total = data.total
  // æ‰“开附件弹窗
  const openFileDialog = async row => {
    currentMaintenanceTaskId.value = row.id;
    fileDialogVisible.value = true;
  };
  onMounted(() => {
    // æ ¹æ®é»˜è®¤æ¿€æ´»çš„ Tab è°ƒç”¨å¯¹åº”的查询接口
    if (activeTab.value === "scheduled") {
      getScheduledTableData();
    } else {
      getTableData();
    }
  } catch (error) {
    console.log(error);
  }
}
const resetFilters = () => {
  filters.deviceName = ''
  filters.maintenancePlanTime = ''
  filters.maintenanceActuallyTime = ''
  filters.maintenanceActuallyName = ''
  getTableData()
}
const handleSelectionChange = (selection) => {
  multipleList.value = selection
}
// æ£€æŸ¥é€‰ä¸­çš„记录中是否有完结状态的
const hasFinishedStatus = computed(() => {
  return multipleList.value.some(item => item.status === 1)
})
const changePage = (page) => {
  pagination.value.currentPage = page.page
  pagination.value.pageSize = page.limit
  getTableData()
}
const addMaintain = (row) => {
  maintainModalRef.value.open(row.id, row)
}
const addPlan = () => {
  planModalRef.value.openModal()
}
const editPlan = (id) => {
  planModalRef.value.openEdit(id)
}
const delRepairByIds = async (ids) => {
  // æ£€æŸ¥æ˜¯å¦æœ‰å®Œç»“状态的记录
  const hasFinished = multipleList.value.some(item => item.status === 1)
  if (hasFinished) {
    ElMessage.warning('不能删除状态为完结的记录')
    return
  }
  try {
    await ElMessageBox.confirm('确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?', '警告', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    })
    const { code } = await delUpkeep(ids)
    if (code === 200) {
      ElMessage.success('删除成功')
      getTableData()
    }
  } catch (error) {
    // ç”¨æˆ·å–消删除
  }
}
const handleOut = () => {
  ElMessageBox.confirm('选中的内容将被导出,是否确认导出?', '导出', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
  })
    .then(() => {
      proxy.download('/device/maintenance/export', {}, '设备保养.xlsx')
    })
    .catch(() => {
      ElMessage.info('已取消')
    })
}
const handleDateChange = (date, type) => {
  if (type === 1) {
    filters.maintenanceActuallyTime = date ? dayjs(date).format('YYYY-MM-DD') : ''
  } else {
    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('获取附件列表失败')
  }
}
// æ‰“开附件弹窗
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
  }
}
onMounted(() => {
  // æ ¹æ®é»˜è®¤æ¿€æ´»çš„ Tab è°ƒç”¨å¯¹åº”的查询接口
  if (activeTab.value === 'scheduled') {
    getScheduledTableData()
  } else {
    getTableData()
  }
})
  });
</script>
<style lang="scss" scoped>
.table_list {
  margin-top: unset;
}
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
}
  .table_list {
    margin-top: unset;
  }
  .actions {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
  }
</style>
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/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,13 +435,16 @@
  if (type === "edit") {
    // ç¼–辑模式,加载现有数据
    Object.assign(borrowForm, data);
    // å­˜å‚¨æ–‡æ¡£åç§°ç”¨äºŽæ˜¾ç¤º
    currentEditDocName.value = data.docName || '';
  } else {
    // æ–°å¢žæ¨¡å¼ï¼Œæ¸…空表单
    Object.keys(borrowForm).forEach(key => {
      borrowForm[key] = "";
    });
         // è®¾ç½®é»˜è®¤çŠ¶æ€
     borrowForm.borrowStatus = "借阅";
    currentEditDocName.value = ''; // æ¸…空编辑时的文档名称
    // è®¾ç½®é»˜è®¤çŠ¶æ€
    borrowForm.borrowStatus = "借阅";
    // è®¾ç½®å½“前日期为借阅日期
    borrowForm.borrowDate = new Date().toISOString().split('T')[0];
  }
@@ -445,6 +455,7 @@
  proxy.$refs.borrowFormRef.resetFields();
  borrowDia.value = false;
  scanContent.value = ''; // æ¸…空扫码内容
  currentEditDocName.value = ''; // æ¸…空编辑时的文档名称
};
// æäº¤å€Ÿé˜…表单
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;
    }
  }
};
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 = ''; // æ¸…空编辑时的文档名称
};
// æäº¤å½’还表单
src/views/financialManagement/assets/fixedAssets.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,482 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="资产编号:">
        <el-input v-model="filters.assetCode" placeholder="请输入资产编号" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="资产名称:">
        <el-input v-model="filters.assetName" placeholder="请输入资产名称" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="资产类别:">
        <el-select v-model="filters.category" placeholder="请选择类别" clearable style="width: 150px;">
          <el-option label="房屋建筑" value="building" />
          <el-option label="机器设备" value="machine" />
          <el-option label="运输工具" value="vehicle" />
          <el-option label="电子设备" value="electronic" />
          <el-option label="办公家具" value="furniture" />
        </el-select>
      </el-form-item>
      <el-form-item label="状态:">
        <el-select v-model="filters.status" placeholder="请选择状态" clearable style="width: 150px;">
          <el-option label="在用" value="in_use" />
          <el-option label="闲置" value="idle" />
          <el-option label="报废" value="scrapped" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <div class="actions">
        <div>
          <el-statistic title="资产原值合计" :value="totalOriginalValue" precision="2" prefix="Â¥" />
          <el-statistic title="累计折旧合计" :value="totalDepreciation" precision="2" prefix="Â¥" style="margin-left: 30px;" />
          <el-statistic title="净值合计" :value="totalNetValue" precision="2" prefix="Â¥" style="margin-left: 30px;" />
        </div>
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增资产</el-button>
          <el-button type="warning" @click="handleDepreciation" icon="Money">折旧计提</el-button>
          <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
        <template #originalValue="{ row }">
          <span class="text-primary">Â¥{{ formatMoney(row.originalValue) }}</span>
        </template>
        <template #accumulatedDepreciation="{ row }">
          <span class="text-warning">Â¥{{ formatMoney(row.accumulatedDepreciation) }}</span>
        </template>
        <template #netValue="{ row }">
          <span class="text-success">Â¥{{ formatMoney(row.netValue) }}</span>
        </template>
        <template #category="{ row }">
          <el-tag>{{ getCategoryLabel(row.category) }}</el-tag>
        </template>
        <template #status="{ row }">
          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
        </template>
        <template #operation="{ row }">
          <el-button type="primary" link @click="view(row)">查看</el-button>
          <el-button type="primary" link @click="edit(row)">编辑</el-button>
          <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="资产编号" prop="assetCode">
              <el-input v-model="form.assetCode" placeholder="系统自动生成" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="资产名称" prop="assetName">
              <el-input v-model="form.assetName" placeholder="请输入资产名称" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="资产类别" prop="category">
              <el-select v-model="form.category" placeholder="请选择资产类别" style="width: 100%;">
                <el-option label="房屋建筑" value="building" />
                <el-option label="机器设备" value="machine" />
                <el-option label="运输工具" value="vehicle" />
                <el-option label="电子设备" value="electronic" />
                <el-option label="办公家具" value="furniture" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号" prop="specification">
              <el-input v-model="form.specification" placeholder="请输入规格型号" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="购置日期" prop="purchaseDate">
              <el-date-picker v-model="form.purchaseDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="资产原值" prop="originalValue">
              <el-input-number v-model="form.originalValue" :min="0" :precision="2" style="width: 100%;" @change="calculateNetValue" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="使用年限" prop="usefulLife">
              <el-input-number v-model="form.usefulLife" :min="1" :max="50" style="width: 100%;" />
              <span style="margin-left: 10px;">å¹´</span>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="残值率" prop="residualRate">
              <el-input-number v-model="form.residualRate" :min="0" :max="10" :precision="2" style="width: 100%;" />
              <span style="margin-left: 10px;">%</span>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="累计折旧">
              <el-input v-model="form.accumulatedDepreciation" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="资产净值">
              <el-input v-model="form.netValue" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="存放地点" prop="location">
              <el-input v-model="form.location" placeholder="请输入存放地点" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="使用部门" prop="department">
              <el-input v-model="form.department" placeholder="请输入使用部门" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="保管人" prop="keeper">
              <el-input v-model="form.keeper" placeholder="请输入保管人" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="状态" prop="status">
              <el-select v-model="form.status" placeholder="请选择状态" style="width: 100%;">
                <el-option label="在用" value="in_use" />
                <el-option label="闲置" value="idle" />
                <el-option label="报废" value="scrapped" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button v-if="!isView" type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import {
  listFixedAssetPage,
  addFixedAsset,
  updateFixedAsset,
  deleteFixedAsset,
  depreciateFixedAsset,
} from "@/api/financialManagement/fixedAsset";
defineOptions({
  name: "固定资产",
});
const filters = reactive({
  assetCode: "",
  assetName: "",
  category: "",
  status: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "资产编号", prop: "assetCode", width: "130" },
  { label: "资产名称", prop: "assetName", width: "150" },
  { label: "资产类别", prop: "category", dataType: "slot", slot: "category" },
  { label: "规格型号", prop: "specification", width: "120" },
  { label: "资产原值", prop: "originalValue", dataType: "slot", slot: "originalValue" },
  { label: "累计折旧", prop: "accumulatedDepreciation", dataType: "slot", slot: "accumulatedDepreciation" },
  { label: "资产净值", prop: "netValue", dataType: "slot", slot: "netValue" },
  { label: "状态", prop: "status", dataType: "slot", slot: "status" },
  { label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
];
const dataList = ref([]);
const multipleList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const isView = ref(false);
const currentId = ref(null);
const selectedIds = computed(() =>
  multipleList.value
    .map(item => item?.id)
    .filter(id => id !== undefined && id !== null && id !== "")
);
const createDefaultForm = () => ({
  assetCode: "",
  assetName: "",
  category: "",
  specification: "",
  purchaseDate: "",
  originalValue: 0,
  usefulLife: 5,
  residualRate: 5,
  accumulatedDepreciation: 0,
  netValue: 0,
  location: "",
  department: "",
  keeper: "",
  status: "in_use",
  remark: "",
});
const form = reactive({
  ...createDefaultForm(),
});
const rules = {
  assetName: [{ required: true, message: "请输入资产名称", trigger: "blur" }],
  category: [{ required: true, message: "请选择资产类别", trigger: "change" }],
  purchaseDate: [{ required: true, message: "请选择购置日期", trigger: "change" }],
  originalValue: [{ required: true, message: "请输入资产原值", trigger: "blur" }],
  usefulLife: [{ required: true, message: "请输入使用年限", trigger: "blur" }],
};
const totalOriginalValue = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
});
const totalDepreciation = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.accumulatedDepreciation), 0);
});
const totalNetValue = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.netValue), 0);
});
const formatMoney = (value) => {
  if (value === undefined || value === null) return "0.00";
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const getCategoryLabel = (category) => {
  const map = {
    building: "房屋建筑",
    machine: "机器设备",
    vehicle: "运输工具",
    electronic: "电子设备",
    furniture: "办公家具",
  };
  return map[category] || category;
};
const getStatusLabel = (status) => {
  const key = String(status || "").toLowerCase();
  const map = { in_use: "在用", idle: "闲置", repair: "维修中", scrapped: "报废" };
  return map[key] || status;
};
const getStatusType = (status) => {
  const key = String(status || "").toLowerCase();
  const map = { in_use: "success", idle: "warning", repair: "warning", scrapped: "info" };
  return map[key] || "";
};
const calculateNetValue = () => {
  const originalValue = Number(form.originalValue || 0);
  const accumulatedDepreciation = Number(form.accumulatedDepreciation || 0);
  form.netValue = Number((originalValue - accumulatedDepreciation).toFixed(2));
};
// è”调约定:分页参数固定为 current/size,返回 data.records/data.total
const getTableData = async () => {
  try {
    const { data } = await listFixedAssetPage({
      current: pagination.currentPage,
      size: pagination.pageSize,
      assetCode: filters.assetCode,
      assetName: filters.assetName,
      category: filters.category,
      status: filters.status,
    });
    dataList.value = data?.records || [];
    multipleList.value = [];
    pagination.total = Number(data?.total || 0);
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
};
const handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
const resetFilters = () => {
  filters.assetCode = "";
  filters.assetName = "";
  filters.category = "";
  filters.status = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const buildAssetCode = () => `GD${Date.now().toString().slice(-10)}`;
const add = () => {
  isEdit.value = false;
  isView.value = false;
  currentId.value = null;
  dialogTitle.value = "新增固定资产";
  Object.assign(form, createDefaultForm(), {
    assetCode: buildAssetCode(),
    purchaseDate: new Date().toISOString().split('T')[0],
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  isView.value = false;
  currentId.value = row.id;
  dialogTitle.value = "编辑固定资产";
  Object.assign(form, createDefaultForm(), row);
  dialogVisible.value = true;
};
const view = (row) => {
  edit(row);
  isView.value = true;
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该固定资产吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    // è”调约定:删除接口使用 ids=1&ids=2
    await deleteFixedAsset([row.id]);
    if (dataList.value.length === 1 && pagination.currentPage > 1) {
      pagination.currentPage -= 1;
    }
    ElMessage.success("删除成功");
    await getTableData();
  });
};
const handleDepreciation = () => {
  const ids = selectedIds.value;
  const confirmText = ids.length
    ? `确认对选中的 ${ids.length} æ¡èµ„产进行本月折旧计提吗?`
    : "确认进行本月折旧计提吗?";
  ElMessageBox.confirm(confirmText, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(async () => {
    await depreciateFixedAsset({ ids });
    ElMessage.success("折旧计提完成");
    await getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  if (isView.value) {
    dialogVisible.value = false;
    return;
  }
  formRef.value.validate(async valid => {
    if (valid) {
      try {
        calculateNetValue();
        const payload = { ...form };
        if (isEdit.value) {
          payload.id = currentId.value;
          await updateFixedAsset(payload);
          ElMessage.success("编辑成功");
        } else {
          await addFixedAsset(payload);
          ElMessage.success("新增成功");
        }
        dialogVisible.value = false;
        await getTableData();
      } catch (error) {
        // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
      }
    }
  });
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  > div:first-child {
    display: flex;
    align-items: center;
  }
}
.text-primary {
  color: #409eff;
  font-weight: bold;
}
.text-warning {
  color: #e6a23c;
  font-weight: bold;
}
.text-success {
  color: #67c23a;
  font-weight: bold;
}
</style>
src/views/financialManagement/assets/intangibleAssets.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,480 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="资产编号:">
        <el-input v-model="filters.assetCode" placeholder="请输入资产编号" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="资产名称:">
        <el-input v-model="filters.assetName" placeholder="请输入资产名称" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="资产类别:">
        <el-select v-model="filters.category" placeholder="请选择类别" clearable style="width: 150px;">
          <el-option label="专利权" value="patent" />
          <el-option label="商标权" value="trademark" />
          <el-option label="著作权" value="copyright" />
          <el-option label="软件" value="software" />
          <el-option label="土地使用权" value="land" />
          <el-option label="其他" value="other" />
        </el-select>
      </el-form-item>
      <el-form-item label="状态:">
        <el-select v-model="filters.status" placeholder="请选择状态" clearable style="width: 150px;">
          <el-option label="在用" value="in_use" />
          <el-option label="闲置" value="idle" />
          <el-option label="已摊销完毕" value="amortized" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <div class="actions">
        <div>
          <el-statistic title="资产原值合计" :value="totalOriginalValue" precision="2" prefix="Â¥" />
          <el-statistic title="累计摊销合计" :value="totalAmortization" precision="2" prefix="Â¥" style="margin-left: 30px;" />
          <el-statistic title="净值合计" :value="totalNetValue" precision="2" prefix="Â¥" style="margin-left: 30px;" />
        </div>
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增资产</el-button>
          <el-button type="warning" @click="handleAmortization" icon="Money">摊销计提</el-button>
          <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
        <template #originalValue="{ row }">
          <span class="text-primary">Â¥{{ formatMoney(row.originalValue) }}</span>
        </template>
        <template #accumulatedAmortization="{ row }">
          <span class="text-warning">Â¥{{ formatMoney(row.accumulatedAmortization) }}</span>
        </template>
        <template #netValue="{ row }">
          <span class="text-success">Â¥{{ formatMoney(row.netValue) }}</span>
        </template>
        <template #category="{ row }">
          <el-tag>{{ getCategoryLabel(row.category) }}</el-tag>
        </template>
        <template #status="{ row }">
          <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
        </template>
        <template #operation="{ row }">
          <el-button type="primary" link @click="view(row)">查看</el-button>
          <el-button type="primary" link @click="edit(row)">编辑</el-button>
          <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="资产编号" prop="assetCode">
              <el-input v-model="form.assetCode" placeholder="系统自动生成" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="资产名称" prop="assetName">
              <el-input v-model="form.assetName" placeholder="请输入资产名称" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="资产类别" prop="category">
              <el-select v-model="form.category" placeholder="请选择资产类别" style="width: 100%;">
                <el-option label="专利权" value="patent" />
                <el-option label="商标权" value="trademark" />
                <el-option label="著作权" value="copyright" />
                <el-option label="软件" value="software" />
                <el-option label="土地使用权" value="land" />
                <el-option label="其他" value="other" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="证书编号" prop="certificateNo">
              <el-input v-model="form.certificateNo" placeholder="请输入证书编号" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="取得日期" prop="acquisitionDate">
              <el-date-picker v-model="form.acquisitionDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="资产原值" prop="originalValue">
              <el-input-number v-model="form.originalValue" :min="0" :precision="2" style="width: 100%;" @change="calculateNetValue" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="摊销年限" prop="amortizationPeriod">
              <el-input-number v-model="form.amortizationPeriod" :min="1" :max="50" style="width: 100%;" />
              <span style="margin-left: 10px;">å¹´</span>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="残值率" prop="residualRate">
              <el-input-number v-model="form.residualRate" :min="0" :max="10" :precision="2" style="width: 100%;" />
              <span style="margin-left: 10px;">%</span>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="累计摊销">
              <el-input v-model="form.accumulatedAmortization" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="资产净值">
              <el-input v-model="form.netValue" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="有效期至" prop="validityDate">
              <el-date-picker v-model="form.validityDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="状态" prop="status">
              <el-select v-model="form.status" placeholder="请选择状态" style="width: 100%;">
                <el-option label="在用" value="in_use" />
                <el-option label="闲置" value="idle" />
                <el-option label="已摊销完毕" value="amortized" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="资产描述" prop="description">
          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入资产描述" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button v-if="!isView" type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import {
  listIntangibleAssetPage,
  addIntangibleAsset,
  updateIntangibleAsset,
  deleteIntangibleAsset,
  amortizeIntangibleAsset,
} from "@/api/financialManagement/intangibleAsset";
defineOptions({
  name: "无形资产",
});
const filters = reactive({
  assetCode: "",
  assetName: "",
  category: "",
  status: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "资产编号", prop: "assetCode", width: "130" },
  { label: "资产名称", prop: "assetName", width: "150" },
  { label: "资产类别", prop: "category", dataType: "slot", slot: "category" },
  { label: "证书编号", prop: "certificateNo", width: "150" },
  { label: "资产原值", prop: "originalValue", dataType: "slot", slot: "originalValue" },
  { label: "累计摊销", prop: "accumulatedAmortization", dataType: "slot", slot: "accumulatedAmortization" },
  { label: "资产净值", prop: "netValue", dataType: "slot", slot: "netValue" },
  { label: "状态", prop: "status", dataType: "slot", slot: "status" },
  { label: "操作", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
];
const dataList = ref([]);
const multipleList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const isView = ref(false);
const currentId = ref(null);
const selectedIds = computed(() =>
  multipleList.value
    .map(item => item?.id)
    .filter(id => id !== undefined && id !== null && id !== "")
);
const createDefaultForm = () => ({
  assetCode: "",
  assetName: "",
  category: "",
  certificateNo: "",
  acquisitionDate: "",
  originalValue: 0,
  amortizationPeriod: 10,
  residualRate: 0,
  accumulatedAmortization: 0,
  netValue: 0,
  validityDate: "",
  status: "in_use",
  description: "",
  remark: "",
});
const form = reactive({
  ...createDefaultForm(),
});
const rules = {
  assetName: [{ required: true, message: "请输入资产名称", trigger: "blur" }],
  category: [{ required: true, message: "请选择资产类别", trigger: "change" }],
  acquisitionDate: [{ required: true, message: "请选择取得日期", trigger: "change" }],
  originalValue: [{ required: true, message: "请输入资产原值", trigger: "blur" }],
  amortizationPeriod: [{ required: true, message: "请输入摊销年限", trigger: "blur" }],
};
const totalOriginalValue = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
});
const totalAmortization = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.accumulatedAmortization), 0);
});
const totalNetValue = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.netValue), 0);
});
const formatMoney = (value) => {
  if (value === undefined || value === null) return "0.00";
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const getCategoryLabel = (category) => {
  const map = {
    patent: "专利权",
    trademark: "商标权",
    copyright: "著作权",
    software: "软件",
    land: "土地使用权",
    other: "其他",
  };
  return map[category] || category;
};
const getStatusLabel = (status) => {
  const key = String(status || "").toLowerCase();
  const map = {
    in_use: "在用",
    idle: "闲置",
    expired: "已到期",
    amortized: "已摊销完毕",
  };
  return map[key] || status;
};
const getStatusType = (status) => {
  const key = String(status || "").toLowerCase();
  const map = { in_use: "success", idle: "warning", expired: "warning", amortized: "info" };
  return map[key] || "";
};
const calculateNetValue = () => {
  const originalValue = Number(form.originalValue || 0);
  const accumulatedAmortization = Number(form.accumulatedAmortization || 0);
  form.netValue = Number((originalValue - accumulatedAmortization).toFixed(2));
};
// è”调约定:分页参数固定为 current/size,返回 data.records/data.total
const getTableData = async () => {
  try {
    const { data } = await listIntangibleAssetPage({
      current: pagination.currentPage,
      size: pagination.pageSize,
      assetCode: filters.assetCode,
      assetName: filters.assetName,
      category: filters.category,
      status: filters.status,
    });
    dataList.value = data?.records || [];
    multipleList.value = [];
    pagination.total = Number(data?.total || 0);
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
};
const handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
const resetFilters = () => {
  filters.assetCode = "";
  filters.assetName = "";
  filters.category = "";
  filters.status = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const buildAssetCode = () => `WX${Date.now().toString().slice(-10)}`;
const add = () => {
  isEdit.value = false;
  isView.value = false;
  currentId.value = null;
  dialogTitle.value = "新增无形资产";
  Object.assign(form, createDefaultForm(), {
    assetCode: buildAssetCode(),
    acquisitionDate: new Date().toISOString().split('T')[0],
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  isView.value = false;
  currentId.value = row.id;
  dialogTitle.value = "编辑无形资产";
  Object.assign(form, createDefaultForm(), row);
  dialogVisible.value = true;
};
const view = (row) => {
  edit(row);
  isView.value = true;
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该无形资产吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    // è”调约定:删除接口使用 ids=1&ids=2
    await deleteIntangibleAsset([row.id]);
    if (dataList.value.length === 1 && pagination.currentPage > 1) {
      pagination.currentPage -= 1;
    }
    ElMessage.success("删除成功");
    await getTableData();
  });
};
const handleAmortization = () => {
  const ids = selectedIds.value;
  const confirmText = ids.length
    ? `确认对选中的 ${ids.length} æ¡èµ„产进行本月摊销计提吗?`
    : "确认进行本月摊销计提吗?";
  ElMessageBox.confirm(confirmText, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(async () => {
    await amortizeIntangibleAsset({ ids });
    ElMessage.success("摊销计提完成");
    await getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  if (isView.value) {
    dialogVisible.value = false;
    return;
  }
  formRef.value.validate(async valid => {
    if (valid) {
      try {
        calculateNetValue();
        const payload = { ...form };
        if (isEdit.value) {
          payload.id = currentId.value;
          await updateIntangibleAsset(payload);
          ElMessage.success("编辑成功");
        } else {
          await addIntangibleAsset(payload);
          ElMessage.success("新增成功");
        }
        dialogVisible.value = false;
        await getTableData();
      } catch (error) {
        // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
      }
    }
  });
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  > div:first-child {
    display: flex;
    align-items: center;
  }
}
.text-primary {
  color: #409eff;
  font-weight: bold;
}
.text-warning {
  color: #e6a23c;
  font-weight: bold;
}
.text-success {
  color: #67c23a;
  font-weight: bold;
}
</style>
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;
    }
  } 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;
  }
};
  recordId.value = row.id
  fileDialogVisible.value = true
}
onMounted(() => {
  getTableData();
src/views/financialManagement/generalLedger/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,498 @@
<template>
  <div class="app-container">
    <el-form :model="filters"
             :inline="true">
      <el-form-item label="科目编码:">
        <el-input v-model="filters.subjectCode"
                  placeholder="请输入科目编码"
                  clearable
                  style="width: 200px;" />
      </el-form-item>
      <el-form-item label="科目名称:">
        <el-input v-model="filters.subjectName"
                  placeholder="请输入科目名称"
                  clearable
                  style="width: 200px;" />
      </el-form-item>
      <el-form-item label="科目类型:">
        <el-select v-model="filters.subjectType"
                   placeholder="请选择"
                   clearable
                   style="width: 200px;">
          <el-option label="资产类"
                     value="资产类" />
          <el-option label="负债类"
                     value="负债类" />
          <el-option label="权益类"
                     value="权益类" />
          <el-option label="成本类"
                     value="成本类" />
          <el-option label="损益类"
                     value="损益类" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary"
                   @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <div class="actions">
        <div></div>
        <div>
          <el-button type="primary"
                     @click="add"
                     icon="Plus">新增</el-button>
          <!-- <el-button @click="handleOut"
                     icon="Download">导出</el-button> -->
        </div>
      </div>
      <el-table ref="tableRef"
                v-loading="loading"
                :data="dataList"
                row-key="id"
                :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
                height="calc(100vh - 280px)"
                border
                stripe
                highlight-current-row
                class="subject-table">
        <el-table-column label="科目编码" prop="subjectCode" width="140">
          <template #default="scope">
            <span class="subject-code">{{ scope.row.subjectCode }}</span>
          </template>
        </el-table-column>
        <el-table-column label="科目名称" prop="subjectName" min-width="180">
          <template #default="scope">
            <span class="subject-name" :class="{ 'is-parent': scope.row.children?.length > 0 }">
              {{ scope.row.subjectName }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="科目类型" prop="subjectType" width="100" align="center">
          <template #default="scope">
            <el-tag size="small" :type="getSubjectTypeType(scope.row.subjectType)">
              {{ scope.row.subjectType }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="余额方向" prop="balanceDirection" width="100" align="center">
          <template #default="scope">
            <el-tag size="small" :type="scope.row.balanceDirection === '借方' ? 'primary' : 'danger'">
              {{ scope.row.balanceDirection }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="状态" prop="status" width="80" align="center">
          <template #default="scope">
            <el-tag size="small" :type="scope.row.status === 0 || scope.row.status === '0' ? 'success' : 'info'">
              {{ scope.row.status === 0 || scope.row.status === '0' ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="备注" prop="remark" show-overflow-tooltip min-width="150" />
        <el-table-column label="操作" align="center" fixed="right" width="240">
          <template #default="scope">
            <el-button link type="primary" icon="Plus" @click="addChild(scope.row)">新增</el-button>
            <el-button link type="primary" icon="Edit" @click="edit(scope.row)">编辑</el-button>
            <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <FormDialog :title="dialogTitle"
                v-model="dialogVisible"
                width="600px"
                @confirm="submitForm"
                @cancel="dialogVisible = false">
      <el-form :model="form"
               :rules="rules"
               ref="formRef"
               label-width="100px">
        <el-form-item label="父级科目">
          <el-input :model-value="parentSubjectLabel"
                    disabled />
        </el-form-item>
        <el-form-item label="科目编码"
                      prop="subjectCode">
          <el-input v-model="form.subjectCode"
                    placeholder="请输入科目编码" />
        </el-form-item>
        <el-form-item label="科目名称"
                      prop="subjectName">
          <el-input v-model="form.subjectName"
                    placeholder="请输入科目名称" />
        </el-form-item>
        <el-form-item label="科目类型"
                      prop="subjectType">
          <el-select v-model="form.subjectType"
                     placeholder="请选择科目类型"
                     style="width: 100%;">
            <el-option label="资产类"
                       value="资产类" />
            <el-option label="负债类"
                       value="负债类" />
            <el-option label="权益类"
                       value="权益类" />
            <el-option label="成本类"
                       value="成本类" />
            <el-option label="损益类"
                       value="损益类" />
          </el-select>
        </el-form-item>
        <el-form-item label="余额方向"
                      prop="balanceDirection">
          <el-radio-group v-model="form.balanceDirection">
            <el-radio label="借方">借方</el-radio>
            <el-radio label="贷方">贷方</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="form.status">
            <el-radio :label="0">启用</el-radio>
            <el-radio :label="1">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注"
                      prop="remark">
          <el-input v-model="form.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入备注" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary"
                   @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
  import { ref, reactive, onMounted, getCurrentInstance, nextTick } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import {
    listAccountSubject,
    addAccountSubject,
    updateAccountSubject,
    delAccountSubject,
    exportAccountSubject,
  } from "@/api/financialManagement/accountSubject";
  defineOptions({
    name: "总帐科目",
  });
  const { proxy } = getCurrentInstance();
  const filters = reactive({
    subjectCode: "",
    subjectName: "",
    subjectType: "",
  });
  const pagination = reactive({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const columns = [
    { label: "科目编码", prop: "subjectCode", width: "120" },
    { label: "科目名称", prop: "subjectName", width: "150" },
    { label: "科目类型", prop: "subjectType" },
    {
      label: "余额方向",
      prop: "balanceDirection",
      dataType: "tag",
      formatData: value => {
        if (value === "借方") {
          return "借方";
        }
        return "贷方";
      },
      formatType: value => {
        if (value === "借方") {
          return "primary";
        }
        return "danger";
      },
    },
    {
      label: "状态",
      prop: "status",
      dataType: "tag",
      formatData: value => {
        if (value === 0 || value === "0") {
          return "启用";
        }
        return "禁用";
      },
      formatType: value => {
        if (value === 0 || value === "0") {
          return "success";
        }
        return "info";
      },
    },
    { label: "备注", prop: "remark", showOverflowTooltip: true },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: "220",
      operation: [
        {
          name: "新增",
          type: "primary",
          clickFun: row => {
            addChild(row);
          },
        },
        {
          name: "编辑",
          type: "primary",
          clickFun: row => {
            edit(row);
          },
        },
        {
          name: "删除",
          type: "danger",
          clickFun: row => {
            handleDelete(row);
          },
        },
      ],
    },
  ];
  const dataList = ref([]);
  const dialogVisible = ref(false);
  const dialogTitle = ref("");
  const parentSubjectLabel = ref("顶级科目");
  const formRef = ref(null);
  const tableRef = ref(null);
  const isEdit = ref(false);
  const loading = ref(false);
  const form = reactive({
    id: undefined,
    parentId: null,
    subjectCode: "",
    subjectName: "",
    subjectType: "",
    balanceDirection: "借方",
    status: 0,
    remark: "",
  });
  const rules = {
    subjectCode: [{ required: true, message: "请输入科目编码", trigger: "blur" }],
    subjectName: [{ required: true, message: "请输入科目名称", trigger: "blur" }],
    subjectType: [
      { required: true, message: "请选择科目类型", trigger: "change" },
    ],
  };
  const getSubjectTypeType = type => {
    const map = {
      èµ„产类: "success",
      è´Ÿå€ºç±»: "danger",
      æƒç›Šç±»: "warning",
      æˆæœ¬ç±»: "info",
      æŸç›Šç±»: "primary",
    };
    return map[type] || "";
  };
  const getTableData = () => {
    loading.value = true;
    const query = {
      current: pagination.currentPage,
      size: pagination.pageSize,
      ...filters,
    };
    listAccountSubject(query).then(response => {
      dataList.value = response.data.records || [];
      loading.value = false;
    }).catch(() => {
      loading.value = false;
    });
  };
  const resetFilters = () => {
    filters.subjectCode = "";
    filters.subjectName = "";
    filters.subjectType = "";
    pagination.currentPage = 1;
    getTableData();
  };
  const changePage = obj => {
    pagination.currentPage = obj.page;
    pagination.pageSize = obj.limit;
    getTableData();
  };
  const buildParentSubjectLabel = parentRow => {
    if (!parentRow) {
      return "顶级科目";
    }
    const code = parentRow.subjectCode || "";
    const name = parentRow.subjectName || "";
    return `${code} ${name}`.trim();
  };
  const resetForm = ({ parentId = null, parentRow = null } = {}) => {
    Object.assign(form, {
      id: undefined,
      parentId,
      subjectCode: "",
      subjectName: "",
      subjectType: "",
      balanceDirection: "借方",
      status: 0,
      remark: "",
    });
    parentSubjectLabel.value = buildParentSubjectLabel(parentRow);
  };
  const add = () => {
    isEdit.value = false;
    dialogTitle.value = "新增科目";
    resetForm({ parentId: null, parentRow: null });
    dialogVisible.value = true;
  };
  const addChild = row => {
    isEdit.value = false;
    dialogTitle.value = "新增子科目";
    resetForm({ parentId: row.id, parentRow: row });
    form.subjectType = row.subjectType || "";
    form.balanceDirection = row.balanceDirection || "借方";
    dialogVisible.value = true;
  };
  const findSubjectById = (nodes, id) => {
    for (const item of nodes || []) {
      if (item.id === id) {
        return item;
      }
      if (item.children && item.children.length > 0) {
        const found = findSubjectById(item.children, id);
        if (found) {
          return found;
        }
      }
    }
    return null;
  };
  const edit = row => {
    isEdit.value = true;
    dialogTitle.value = "编辑科目";
    Object.assign(form, row);
    form.parentId = row.parentId ?? null;
    const parentRow =
      row.parentId === null || row.parentId === undefined
        ? null
        : findSubjectById(dataList.value, row.parentId);
    parentSubjectLabel.value = parentRow
      ? buildParentSubjectLabel(parentRow)
      : row.parentId
      ? `上级ID: ${row.parentId}`
      : buildParentSubjectLabel(null);
    dialogVisible.value = true;
  };
  const submitForm = () => {
    formRef.value.validate(valid => {
      if (valid) {
        if (isEdit.value) {
          updateAccountSubject(form).then(() => {
            ElMessage.success("编辑成功");
            dialogVisible.value = false;
            getTableData();
          });
        } else {
          addAccountSubject(form).then(() => {
            ElMessage.success("新增成功");
            dialogVisible.value = false;
            getTableData();
          });
        }
      }
    });
  };
  const handleDelete = row => {
    const ids = row.id;
    ElMessageBox.confirm("确认删除该科目吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        return delAccountSubject(ids);
      })
      .then(() => {
        ElMessage.success("删除成功");
        getTableData();
      });
  };
  const handleOut = () => {
    proxy.download(
      "accountSubject/export",
      {
        ...filters,
      },
      `account_subject_${new Date().getTime()}.xlsx`
    );
  };
  onMounted(() => {
    getTableData();
  });
</script>
<style lang="scss" scoped>
  .actions {
    display: flex;
    justify-content: space-between;
    margin-bottom: 15px;
  }
  .subject-table {
    border-radius: 8px;
    overflow: hidden;
    :deep(.el-table__row) {
      transition: background-color 0.3s;
    }
    :deep(.el-table__row:hover) {
      background-color: #f5f7fa;
    }
    .subject-code {
      color: #606266;
    }
    .subject-name {
      font-weight: 500;
      &.is-parent {
        color: #409eff;
      }
    }
  }
</style>
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 src/views/financialManagement/payable/paymentApply.vue src/views/financialManagement/payable/purchaseIn.vue src/views/financialManagement/payable/purchaseReturn.vue src/views/financialManagement/payable/reconciliation.vue src/views/financialManagement/receivable/invoiceApply.vue src/views/financialManagement/receivable/outputInvoice.vue src/views/financialManagement/receivable/receipt.vue src/views/financialManagement/receivable/reconciliation.vue 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/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/receiptManagement/Record.vue src/views/inventoryManagement/receiptManagement/index.vue src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.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/stockReport/index.vue src/views/lavorissue/ledger/filesDia.vue src/views/lavorissue/statistics/index.vue src/views/oaSystem/projectManagement/components/milestoneList.vue (已删除) src/views/oaSystem/projectManagement/components/phaseGoalList.vue (已删除) src/views/oaSystem/projectManagement/components/projectForm.vue (已删除) src/views/oaSystem/projectManagement/components/taskTree.vue (已删除) src/views/oaSystem/projectManagement/index.vue (已删除) src/views/oaSystem/projectManagement/projectDetail.vue (已删除) src/views/personnelManagement/contractManagement/filesDia.vue src/views/personnelManagement/contractManagement/index.vue src/views/personnelManagement/dimission/components/formDia.vue src/views/personnelManagement/dimission/index.vue src/views/personnelManagement/employeeRecord/index.vue src/views/personnelManagement/socialSecuritySet/index.vue src/views/procurementManagement/paymentEntry/index.vue src/views/procurementManagement/paymentLedger/index.vue src/views/procurementManagement/procurementInvoiceLedger/index.vue src/views/procurementManagement/procurementLedger/fileList.vue src/views/procurementManagement/procurementLedger/index.vue src/views/procurementManagement/procurementReport/index.vue src/views/procurementManagement/purchaseReturnOrder/New.vue src/views/procurementManagement/purchaseReturnOrder/ProductList.vue src/views/procurementManagement/purchaseReturnOrder/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/index.vue src/views/productionManagement/productionReporting/index.vue src/views/productionManagement/productionTraceability/index.vue src/views/productionManagement/workOrder/components/filesDia.vue src/views/productionManagement/workOrder/index.vue src/views/productionManagement/workOrderEdit/index.vue src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue src/views/productionManagement/workOrderManagement/components/filesDia.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/qualityManagement/finalInspection/components/filesDia.vue src/views/qualityManagement/finalInspection/index.vue src/views/qualityManagement/nonconformingManagement/index.vue src/views/qualityManagement/processInspection/components/filesDia.vue src/views/qualityManagement/processInspection/components/formDia.vue src/views/qualityManagement/processInspection/index.vue src/views/qualityManagement/rawMaterialInspection/components/filesDia.vue src/views/qualityManagement/rawMaterialInspection/index.vue src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue src/views/reportAnalysis/PSIDataAnalysis/index.vue src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue src/views/reportAnalysis/dataDashboard/index.vue src/views/reportAnalysis/dataDashboard/index0.vue src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue src/views/reportAnalysis/productionAnalysis/components/center-center.vue src/views/reportAnalysis/productionAnalysis/components/center-top.vue src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue src/views/reportAnalysis/productionAnalysis/components/left-top.vue src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue src/views/reportAnalysis/productionAnalysis/components/right-top.vue src/views/reportAnalysis/productionAnalysis/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/fileList.vue src/views/safeProduction/safeWorkApproval/index.vue src/views/safeProduction/safetyTrainingAssessment/index.vue src/views/salesManagement/deliveryLedger/index.vue src/views/salesManagement/invoiceLedger/index.vue src/views/salesManagement/receiptPaymentLedger/index.vue src/views/salesManagement/returnOrder/components/detailDia.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/system/appVersion/index.vue src/views/systemArchitecture/index.vue src/views/tool/build/CodeTypeDialog.vue vite.config.js