From aaf8a50511d77dbc209ca93bbba308c21179a8bc Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期二, 31 三月 2026 15:38:47 +0800
Subject: [PATCH] #前端
---
rsf-design/src/api/system-manage.js | 443
rsf-design/src/views/basic-info/task-path-template-merge/modules/task-path-template-merge-detail-drawer.vue | 83
rsf-design/src/views/manager/qly-ispt-item/qlyIsptItemTable.columns.js | 150
rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js | 260
rsf-design/src/views/manager/task-item-log/taskItemLogTable.columns.js | 164
rsf-design/src/views/manager/qly-ispt-item/index.vue | 245
rsf-design/src/views/orders/asn-order-log/asnOrderLogTable.columns.js | 313
rsf-design/src/views/orders/wave-item/modules/wave-item-detail-drawer.vue | 55
rsf-design/src/views/stock/stock-transfer/stockTransferPage.helpers.js | 136
rsf-design/src/views/basic-info/loc-area/modules/loc-area-dialog.vue | 162
rsf-design/src/views/orders/wait-pakin-item/index.vue | 333
rsf-design/src/views/basic-info/loc-area-mat-rela/index.vue | 471
rsf-design/src/views/system/task-instance-node/modules/task-instance-node-detail-drawer.vue | 45
rsf-design/src/views/manager/task-item/taskItemPage.helpers.js | 203
rsf-design/src/views/manager/task-log/taskLogPage.helpers.js | 162
rsf-design/src/views/basic-info/loc-type/modules/loc-type-detail-drawer.vue | 57
rsf-design/src/views/basic-info/bas-container/basContainerPage.helpers.js | 342
rsf-design/src/views/orders/delivery/deliveryTable.columns.js | 122
rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-detail-drawer.vue | 60
rsf-design/src/views/basic-info/contract/modules/contract-dialog.vue | 156
rsf-design/src/views/orders/wait-pakin-log/waitPakinLogTable.columns.js | 137
rsf-design/src/views/manager/menu-pda/menuPdaTable.columns.js | 97
rsf-design/src/views/manager/stock/index.vue | 278
rsf-design/src/views/system/tenant/index.vue | 283
rsf-design/src/api/loc-area.js | 142
rsf-design/src/views/orders/check-item/index.vue | 92
rsf-design/src/api/device-bind.js | 167
rsf-design/src/views/system/flow-step-template/flowStepTemplatePage.helpers.js | 165
rsf-design/src/views/orders/transfer-item/transferItemTable.columns.js | 164
rsf-design/src/views/orders/asn-order/index.vue | 399
rsf-design/src/views/system/config/configPage.helpers.js | 119
rsf-design/src/views/basic-info/matnr-group/matnrGroupTable.columns.js | 131
rsf-design/src/views/statistics/in-statistic/inStatisticPage.helpers.js | 107
rsf-design/src/views/manager/revise-log-item/reviseLogItemTable.columns.js | 260
rsf-design/src/api/ai-config.js | 225
rsf-design/src/views/basic-info/device-site/deviceSiteTable.columns.js | 150
rsf-design/src/views/system/serial-rule-item/index.vue | 430
rsf-design/src/views/system/flow-step-instance/flowStepInstanceTable.columns.js | 35
rsf-design/src/views/reports/statistic-count/statisticCountTable.columns.js | 44
rsf-design/src/views/orders/wave/modules/wave-public-task-dialog.vue | 68
rsf-design/src/views/basic-info/bas-station-area/basStationAreaTable.columns.js | 176
rsf-design/src/views/basic-info/loc-area-mat-rela/locAreaMatRelaPage.helpers.js | 389
rsf-design/src/views/basic-info/task-path-template-node/index.vue | 362
rsf-design/src/views/manager/task-item/index.vue | 265
rsf-design/src/views/orders/transfer/modules/transfer-detail-drawer.vue | 93
rsf-design/src/views/system/fields-item/modules/fields-item-detail-drawer.vue | 52
rsf-design/src/views/orders/wave-item/index.vue | 202
rsf-design/src/views/orders/check-item/checkOrderItemPage.helpers.js | 55
rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js | 245
rsf-design/src/views/system/dept/deptTable.columns.js | 69
rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js | 146
rsf-design/src/views/basic-info/loc-area-mat/index.vue | 365
rsf-design/src/views/orders/purchase/modules/purchase-detail-drawer.vue | 91
rsf-design/src/views/manager/stock/modules/stock-detail-drawer.vue | 36
rsf-design/src/views/system/dict-type/index.vue | 225
rsf-design/src/views/system/user/index.vue | 15
rsf-design/src/views/system/serial-rule/serialRulePage.helpers.js | 124
rsf-design/src/views/system/task-instance-node/index.vue | 233
rsf-design/src/views/system/operation-record/operationRecordTable.columns.js | 76
rsf-design/src/api/preparation-item.js | 78
rsf-design/src/views/basic-info/matnr-group/modules/matnr-group-dialog.vue | 202
rsf-design/src/views/orders/wait-pakin-item/waitPakinItemPage.helpers.js | 220
rsf-design/src/api/wave.js | 145
rsf-design/src/router/index.js | 2
rsf-design/src/views/manager/wave-rule/waveRuleTable.columns.js | 73
rsf-design/src/views/manager/task-item/modules/task-item-detail-drawer.vue | 56
rsf-design/src/views/basic-info/loc-area-mat-rela/modules/loc-area-mat-rela-detail-drawer.vue | 60
rsf-design/src/views/manager/revise-log/reviseLogTable.columns.js | 127
rsf-design/src/views/system/fields-item/modules/fields-item-dialog.vue | 174
rsf-design/src/views/orders/delivery/deliveryPage.helpers.js | 264
rsf-design/src/views/orders/asn-order/asnOrderTable.columns.js | 296
rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue | 74
rsf-design/src/views/orders/preparation/index.vue | 385
rsf-design/src/views/manager/freeze/freezeTable.columns.js | 101
rsf-design/src/views/orders/asn-order-item/modules/asn-order-item-detail-drawer.vue | 107
rsf-design/src/api/bas-station.js | 190
rsf-design/src/views/basic-info/companys/modules/companys-dialog.vue | 247
rsf-design/src/views/statistics/out-statistic-item/modules/out-statistic-item-detail-drawer.vue | 57
rsf-design/src/views/system/dict-type/dictTypePage.helpers.js | 92
rsf-design/src/api/purchase.js | 118
rsf-design/src/views/manager/wave-rule/modules/wave-rule-detail-drawer.vue | 41
rsf-design/src/locales/langs/en.json | 13
rsf-design/src/views/basic-info/device-site/modules/device-site-dialog.vue | 137
rsf-design/src/views/system/ai-param/index.vue | 392
rsf-design/src/views/basic-info/warehouse/warehouseTable.columns.js | 118
rsf-design/src/views/work/out-bound/outBoundPage.helpers.js | 239
rsf-design/src/api/bas-container.js | 199
rsf-design/src/views/manager/task/taskPage.helpers.js | 146
rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-detail-drawer.vue | 47
rsf-design/src/api/loc-preview.js | 51
rsf-design/src/views/manager/qly-ispt-item-result/index.vue | 74
rsf-design/src/views/manager/loc-dead-report/modules/loc-dead-report-detail-drawer.vue | 82
rsf-design/src/utils/backend-menu-title.js | 2
rsf-design/src/api/wave-item.js | 71
rsf-design/src/views/basic-info/task-path-template-node/modules/task-path-template-node-detail-drawer.vue | 88
rsf-design/src/router/guards/beforeEach.js | 27
rsf-design/src/api/loc-area-mat-rela.js | 228
rsf-design/src/views/statistics/out-statistic/index.vue | 140
rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js | 93
rsf-design/src/api/asn-order-log.js | 108
rsf-design/src/views/system/tenant/modules/tenant-init-dialog.vue | 219
rsf-design/src/views/system/serial-rule-item/modules/serial-rule-item-detail-drawer.vue | 44
rsf-design/src/api/warehouse-areas-item.js | 80
rsf-design/src/api/loc-area-mat.js | 240
rsf-design/src/views/orders/check/checkOrderTable.columns.js | 134
rsf-design/src/views/manager/loc-revise/modules/loc-revise-detail-drawer.vue | 92
rsf-design/src/views/system/flow-instance/modules/flow-instance-detail-drawer.vue | 35
rsf-design/src/views/basic-info/bas-container/modules/bas-container-detail-drawer.vue | 83
rsf-design/src/views/orders/asn-order-log/modules/asn-order-log-detail-drawer.vue | 78
rsf-design/src/views/orders/delivery-item/deliveryItemTable.columns.js | 141
rsf-design/src/views/basic-info/matnr-group/matnrGroupPage.helpers.js | 311
rsf-design/src/views/system/serial-rule/index.vue | 223
rsf-design/src/views/system/dept/modules/dept-dialog.vue | 183
rsf-design/src/api/stock-transfer.js | 87
rsf-design/src/api/loc-area-rela.js | 157
rsf-design/src/views/system/flow-step-instance/flowStepInstancePage.helpers.js | 162
rsf-design/src/store/modules/worktab.js | 39
rsf-design/src/utils/sys/requestGuard.js | 66
rsf-design/src/views/system/ai-observe/aiObservePage.helpers.js | 165
rsf-design/src/views/dashboard/console/index.vue | 542
rsf-design/src/views/system/host/hostPage.helpers.js | 89
rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue | 52
rsf-design/src/views/basic-info/companys/companysPage.helpers.js | 252
rsf-design/src/views/basic-info/loc-area/index.vue | 345
rsf-design/src/views/orders/asn-order-item/index.vue | 386
rsf-design/src/views/system/operation-record/modules/operation-record-detail-drawer.vue | 53
rsf-design/src/views/basic-info/warehouse/index.vue | 325
rsf-design/src/views/basic-info/device-site/modules/device-site-init-dialog.vue | 212
rsf-design/src/views/basic-info/companys/index.vue | 355
rsf-design/src/views/orders/asn-order-item/asnOrderItemTable.columns.js | 66
rsf-design/src/views/orders/check/checkOrderPage.helpers.js | 135
rsf-design/src/views/orders/wave/modules/wave-detail-drawer.vue | 75
rsf-design/src/views/system/tenant/modules/tenant-detail-drawer.vue | 39
rsf-design/src/views/orders/purchase/purchaseTable.columns.js | 179
rsf-design/src/views/system/task-instance/taskInstancePage.helpers.js | 204
rsf-design/src/views/orders/out-stock/index.vue | 318
rsf-design/src/views/basic-info/bas-station/modules/bas-station-detail-drawer.vue | 130
rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-dialog.vue | 198
rsf-design/src/views/orders/preparation/preparationPage.helpers.js | 250
rsf-design/src/api/out-statistic-item.js | 43
rsf-design/src/views/system/flow-step-instance/index.vue | 117
rsf-design/src/views/basic-info/loc-type/modules/loc-type-dialog.vue | 161
rsf-design/src/views/orders/check-diff-item/checkDiffItemPage.helpers.js | 91
rsf-design/src/views/system/flow-instance/index.vue | 117
rsf-design/src/views/work/check-out-bound/checkOutBoundPage.helpers.js | 178
rsf-design/src/views/manager/task-item-log/taskItemLogPage.helpers.js | 249
rsf-design/src/views/basic-info/contract/contractPage.helpers.js | 162
rsf-design/src/views/orders/wait-pakin-item/waitPakinItemTable.columns.js | 145
rsf-design/src/views/manager/in-statistic-item/inStatisticItemPage.helpers.js | 86
rsf-design/src/views/system/flow-step-log/flowStepLogPage.helpers.js | 121
rsf-design/src/views/basic-info/loc-area-rela/locAreaRelaPage.helpers.js | 193
rsf-design/src/views/basic-info/task-path-template/taskPathTemplatePage.helpers.js | 282
rsf-design/src/views/orders/check/index.vue | 249
rsf-design/src/views/system/flow-instance/flowInstanceTable.columns.js | 35
rsf-design/src/views/orders/preparation-item/preparationItemPage.helpers.js | 111
rsf-design/src/views/system/subsystem-flow-template/subsystemFlowTemplateTable.columns.js | 48
rsf-design/src/views/system/task-instance-node/taskInstanceNodePage.helpers.js | 176
rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-dialog.vue | 307
rsf-design/src/views/manager/loc-item/modules/loc-item-detail-drawer.vue | 71
rsf-design/src/views/work/check-out-bound/index.vue | 310
rsf-design/src/views/basic-info/matnr-group/index.vue | 501
rsf-design/src/views/system/dict-type/dictTypeTable.columns.js | 74
rsf-design/src/api/task.js | 95
rsf-design/src/views/basic-info/loc-area-rela/locAreaRelaTable.columns.js | 115
rsf-design/src/api/check-order.js | 102
rsf-design/src/views/abnormal/abnormalPage.helpers.js | 174
rsf-design/src/views/manager/loc-preview/index.vue | 333
rsf-design/src/views/manager/menu-pda/modules/menu-pda-dialog.vue | 273
rsf-design/src/api/asn-order.js | 129
rsf-design/src/api/device-site.js | 213
rsf-design/src/views/manager/wave-rule/modules/wave-rule-dialog.vue | 153
rsf-design/src/views/basic-info/task-path-template-merge/taskPathTemplateMergeTable.columns.js | 200
rsf-design/src/views/basic-info/loc-area-mat-rela/modules/loc-area-mat-rela-dialog.vue | 261
rsf-design/src/views/system/dict-type/modules/dict-type-detail-drawer.vue | 41
rsf-design/src/api/out-bound.js | 103
rsf-design/src/api/flow-step-template.js | 69
rsf-design/src/api/task-path-template-node.js | 175
rsf-design/src/views/orders/purchase/modules/purchase-dialog.vue | 261
rsf-design/src/views/system/ai-param/modules/ai-param-dialog.vue | 325
rsf-design/src/views/orders/wait-pakin-log/waitPakinLogPage.helpers.js | 165
rsf-design/src/views/manager/qly-inspect/qlyInspectTable.columns.js | 77
rsf-design/src/views/system/flow-instance/flowInstancePage.helpers.js | 175
rsf-design/src/views/statistics/in-statistic/modules/in-statistic-detail-drawer.vue | 48
rsf-design/src/components/core/layouts/art-breadcrumb/index.vue | 4
rsf-design/src/views/manager/loc-dead-report/locDeadReportPage.helpers.js | 198
rsf-design/src/views/statistics/out-statistic/outStatisticTable.columns.js | 91
rsf-design/src/views/manager/qly-inspect/modules/qly-inspect-items-drawer.vue | 44
rsf-design/src/views/statistics/out-statistic/modules/out-statistic-detail-drawer.vue | 57
rsf-design/src/views/basic-info/warehouse/modules/warehouse-detail-drawer.vue | 57
rsf-design/src/api/task-instance.js | 69
rsf-design/src/views/basic-info/loc-area-rela/modules/loc-area-rela-detail-drawer.vue | 63
rsf-design/src/views/statistics/in-statistic-item/inStatisticItemPage.helpers.js | 86
rsf-design/src/views/stock/stock-transfer/modules/stock-transfer-detail-drawer.vue | 78
rsf-design/src/views/system/ai-prompt/modules/ai-prompt-dialog.vue | 303
rsf-design/src/views/system/task-instance/index.vue | 242
rsf-design/src/views/stock/warehouse-areas-item/warehouseAreasItemTable.columns.js | 113
rsf-design/src/views/statistics/out-statistic-item/index.vue | 148
rsf-design/src/api/loc-type.js | 139
rsf-design/src/views/manager/freeze/index.vue | 227
rsf-design/src/api/loc-revise.js | 139
rsf-design/src/views/manager/revise-log/reviseLogPage.helpers.js | 151
rsf-design/src/views/system/serial-rule/modules/serial-rule-detail-drawer.vue | 45
rsf-design/src/views/basic-info/device-site/index.vue | 439
rsf-design/src/views/system/serial-rule-item/serialRuleItemPage.helpers.js | 206
rsf-design/src/views/dashboard/console/consolePage.helpers.js | 164
rsf-design/src/views/system/flow-step-template/index.vue | 185
rsf-design/src/views/orders/wait-pakin/waitPakinTable.columns.js | 122
rsf-design/src/views/orders/wave/wavePage.helpers.js | 312
rsf-design/src/views/system/task-instance-node/taskInstanceNodeTable.columns.js | 97
rsf-design/src/api/stock-item.js | 82
rsf-design/src/views/system/subsystem-flow-template/modules/subsystem-flow-template-detail-drawer.vue | 33
rsf-design/src/views/basic-info/bas-container/modules/bas-container-dialog.vue | 171
rsf-design/src/views/basic-info/device-bind/deviceBindTable.columns.js | 141
rsf-design/src/views/system/flow-step-log/modules/flow-step-log-detail-drawer.vue | 39
rsf-design/src/views/system/host/index.vue | 210
rsf-design/src/views/system/ai-mcp-mount/aiMcpMountPage.helpers.js | 122
rsf-design/src/views/orders/purchase/index.vue | 483
rsf-design/src/views/basic-info/loc-area-rela/index.vue | 361
rsf-design/src/views/stock/warehouse-areas-item/index.vue | 482
rsf-design/src/views/system/serial-rule-item/modules/serial-rule-item-dialog.vue | 194
rsf-design/src/views/manager/in-statistic-item/inStatisticItemTable.columns.js | 117
rsf-design/src/views/system/fields/index.vue | 233
rsf-design/src/views/orders/wave/index.vue | 416
rsf-design/src/views/system/serial-rule-item/serialRuleItemTable.columns.js | 103
rsf-design/src/views/manager/freeze/freezePage.helpers.js | 98
rsf-design/src/views/system/serial-rule/modules/serial-rule-dialog.vue | 195
rsf-design/src/api/purchase-item.js | 95
rsf-design/src/api/subsystem-flow-template.js | 69
rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js | 186
rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue | 154
rsf-design/src/views/system/menu/menuTable.columns.js | 134
rsf-design/src/api/asn-order-item.js | 78
rsf-design/src/views/orders/transfer/transferTable.columns.js | 199
rsf-design/src/views/orders/wait-pakin-item-log/waitPakinItemLogTable.columns.js | 131
rsf-design/src/views/manager/task-item-log/modules/task-item-log-detail-drawer.vue | 72
rsf-design/src/views/orders/check-diff-item/checkDiffItemTable.columns.js | 108
rsf-design/src/views/orders/check-diff/checkDiffPage.helpers.js | 124
rsf-design/src/views/orders/check-diff-item/index.vue | 224
rsf-design/src/store/modules/menu.js | 3
rsf-design/src/views/manager/loc-dead-report/index.vue | 402
rsf-design/src/locales/langs/zh.json | 15
rsf-design/src/views/basic-info/companys/modules/companys-detail-drawer.vue | 65
rsf-design/src/views/orders/wait-pakin/modules/wait-pakin-site-dialog.vue | 189
rsf-design/src/views/orders/out-stock/outStockTable.columns.js | 113
rsf-design/src/views/orders/wait-pakin/waitPakinPage.helpers.js | 253
rsf-design/src/views/statistics/in-statistic/inStatisticTable.columns.js | 86
rsf-design/src/views/manager/stock/stockPage.helpers.js | 103
rsf-design/src/views/reports/statistic-count/statisticCountPage.helpers.js | 55
rsf-design/src/views/orders/wait-pakin-item-log/modules/wait-pakin-item-log-detail-drawer.vue | 70
rsf-design/src/api/task-log.js | 69
rsf-design/src/views/statistics/in-statistic-item/modules/in-statistic-item-detail-drawer.vue | 53
rsf-design/src/views/basic-info/loc-area/locAreaTable.columns.js | 108
rsf-design/src/views/basic-info/bas-station/index.vue | 472
rsf-design/src/views/orders/transfer/index.vue | 474
rsf-design/src/api/flow-step-instance.js | 69
rsf-design/src/api/transfer.js | 137
rsf-design/src/views/system/task-instance/taskInstanceTable.columns.js | 97
rsf-design/src/views/orders/check-diff/index.vue | 281
rsf-design/src/api/delivery.js | 200
rsf-design/src/views/system/menu/menuPage.helpers.js | 164
rsf-design/src/views/basic-info/loc-area-mat/locAreaMatPage.helpers.js | 449
rsf-design/src/views/manager/qly-inspect/qlyInspectPage.helpers.js | 125
rsf-design/src/views/system/subsystem-flow-template/subsystemFlowTemplatePage.helpers.js | 163
rsf-design/src/views/reports/statistic-count/index.vue | 167
rsf-design/src/api/task-path-template-merge.js | 298
rsf-design/src/api/freeze.js | 45
rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-dialog.vue | 294
rsf-design/src/views/manager/task/index.vue | 352
rsf-design/src/api/statistic-count.js | 33
rsf-design/src/views/manager/task-log/index.vue | 250
rsf-design/src/views/manager/task/modules/task-detail-drawer.vue | 59
rsf-design/src/views/system/config/index.vue | 226
rsf-design/src/views/manager/task-log/modules/task-log-detail-drawer.vue | 54
rsf-design/src/api/task-item-log.js | 69
rsf-design/src/views/basic-info/bas-container/basContainerTable.columns.js | 136
rsf-design/src/views/statistics/in-statistic-item/index.vue | 86
rsf-design/src/views/system/tenant/tenantPage.helpers.js | 148
rsf-design/src/views/work/check-out-bound/modules/check-out-bound-detail-drawer.vue | 65
rsf-design/src/views/manager/loc-item/locItemPage.helpers.js | 169
rsf-design/src/api/check-diff.js | 109
rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-bind-matnr-dialog.vue | 212
rsf-design/src/views/statistics/out-statistic/outStatisticPage.helpers.js | 116
rsf-design/src/views/system/flow-step-instance/modules/flow-step-instance-detail-drawer.vue | 40
rsf-design/src/views/system/serial-rule/serialRuleTable.columns.js | 92
rsf-design/src/views/basic-info/task-path-template-node/modules/task-path-template-node-dialog.vue | 283
rsf-design/src/views/manager/loc-revise/locReviseTable.columns.js | 114
rsf-design/src/views/basic-info/contract/index.vue | 316
rsf-design/src/views/manager/task-item-log/index.vue | 389
rsf-design/src/router/core/RouteRegistry.js | 34
rsf-design/src/views/orders/wave-item/waveItemTable.columns.js | 180
rsf-design/src/views/system/config/modules/config-dialog.vue | 181
rsf-design/src/api/out-statistic.js | 43
rsf-design/src/views/orders/wait-pakin/index.vue | 381
rsf-design/src/views/basic-info/loc-area-rela/modules/loc-area-rela-dialog.vue | 152
rsf-design/src/views/manager/stock-item/modules/stock-item-detail-drawer.vue | 82
rsf-design/src/api/serial-rule-item.js | 157
rsf-design/src/views/system/ai-observe/aiObserveTable.columns.js | 78
rsf-design/src/views/system/ai-observe/modules/ai-observe-detail-drawer.vue | 110
rsf-design/src/views/system/task-instance/modules/task-instance-detail-drawer.vue | 45
rsf-design/src/views/manager/revise-log-item/modules/revise-log-item-detail-drawer.vue | 48
rsf-design/src/views/orders/out-stock-item/modules/out-stock-item-detail-drawer.vue | 87
rsf-design/src/api/qly-ispt-item.js | 110
rsf-design/src/views/orders/asn-order-item-log/asnOrderItemLogPage.helpers.js | 169
rsf-design/src/views/system/flow-step-template/modules/flow-step-template-detail-drawer.vue | 33
rsf-design/src/views/orders/transfer-item/transferItemPage.helpers.js | 205
rsf-design/src/views/orders/preparation/preparationTable.columns.js | 55
rsf-design/src/views/orders/asn-order-log/index.vue | 468
.tmp-menu.json | 931
rsf-design/src/views/orders/transfer-item/index.vue | 429
rsf-design/src/views/basic-info/loc-type/locTypePage.helpers.js | 175
rsf-design/src/views/manager/freeze/modules/freeze-detail-drawer.vue | 39
rsf-design/src/views/system/host/modules/host-detail-drawer.vue | 37
rsf-design/src/views/system/ai-param/aiParamPage.helpers.js | 225
rsf-design/src/components/biz/list-export-print/index.vue | 11
rsf-design/src/api/in-statistic-item.js | 49
rsf-design/src/api/task-item.js | 69
rsf-design/src/views/manager/stock-item/stockItemTable.columns.js | 177
rsf-design/src/api/flow-instance.js | 69
rsf-design/src/views/orders/delivery-item/modules/delivery-item-detail-drawer.vue | 74
rsf-design/src/api/wait-pakin-log.js | 85
rsf-design/src/views/manager/loc-preview/locPreviewTable.columns.js | 77
rsf-design/src/views/orders/delivery/modules/delivery-detail-drawer.vue | 178
rsf-design/src/views/work/out-bound/outBoundTable.columns.js | 203
rsf-design/src/views/system/ai-prompt/index.vue | 316
rsf-design/src/views/system/common/usePrintExportPage.js | 20
rsf-design/src/views/system/config/modules/config-detail-drawer.vue | 42
rsf-design/src/views/orders/check-item/modules/check-order-item-detail-drawer.vue | 43
rsf-design/src/views/system/operation-record/index.vue | 281
rsf-design/src/views/basic-info/companys/companysTable.columns.js | 102
rsf-design/src/views/basic-info/loc-type/index.vue | 342
rsf-design/src/views/manager/stock/stockTable.columns.js | 86
rsf-design/src/views/statistics/out-statistic-item/outStatisticItemTable.columns.js | 133
rsf-design/src/api/out-stock-item.js | 97
rsf-design/src/views/orders/check-diff/checkDiffTable.columns.js | 69
rsf-design/src/views/orders/check/modules/check-order-detail-drawer.vue | 71
rsf-design/src/views/manager/loc-preview/locPreviewPage.helpers.js | 127
rsf-design/src/views/basic-info/device-bind/deviceBindPage.helpers.js | 314
rsf-design/src/views/orders/purchase/purchasePage.helpers.js | 314
rsf-design/src/views/stock/warehouse-areas-item/warehouseAreasItemPage.helpers.js | 201
rsf-design/src/views/orders/transfer/modules/transfer-dialog.vue | 184
rsf-design/src/views/basic-info/task-path-template-node/taskPathTemplateNodeTable.columns.js | 145
rsf-design/src/views/work/check-out-bound/checkOutBoundTable.columns.js | 94
rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-detail-drawer.vue | 69
rsf-design/src/views/orders/check-diff-item/modules/check-diff-item-detail-drawer.vue | 42
rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-bind-loc-dialog.vue | 199
rsf-design/src/api/wait-pakin-item-log.js | 104
rsf-design/src/api/contract.js | 112
rsf-design/src/views/statistics/in-statistic/index.vue | 91
rsf-design/src/views/stock/stock-transfer/stockTransferTable.columns.js | 83
rsf-design/src/views/manager/loc-revise/locRevisePage.helpers.js | 233
rsf-design/src/views/manager/stock-item/index.vue | 492
rsf-design/src/views/system/fields-item/index.vue | 232
rsf-design/src/views/orders/preparation-item/index.vue | 300
rsf-design/src/views/basic-info/device-bind/index.vue | 434
rsf-design/src/views/basic-info/bas-station/modules/bas-station-dialog.vue | 292
rsf-design/src/views/system/dept/index.vue | 211
rsf-design/src/api/loc-dead-report.js | 69
rsf-design/src/views/orders/wait-pakin/modules/wait-pakin-detail-drawer.vue | 78
rsf-design/src/views/system/tenant/tenantTable.columns.js | 80
rsf-design/src/views/manager/revise-log-item/index.vue | 174
rsf-design/src/views/basic-info/task-path-template/index.vue | 428
rsf-design/src/views/system/dept/deptPage.helpers.js | 137
rsf-design/src/api/task-path-template.js | 197
rsf-design/src/views/basic-info/bas-station-area/index.vue | 459
rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue | 48
rsf-design/src/views/manager/in-statistic-item/index.vue | 157
rsf-design/src/views/stock/warehouse-areas-item/modules/warehouse-ispt-result-drawer.vue | 44
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java | 134
rsf-design/src/views/orders/asn-order-log/asnOrderLogPage.helpers.js | 306
rsf-design/src/views/basic-info/bas-station/basStationPage.helpers.js | 531
rsf-design/src/views/system/ai-mcp-mount/index.vue | 347
rsf-design/src/api/loc-item.js | 81
rsf-design/src/views/orders/transfer-item/modules/transfer-item-detail-drawer.vue | 78
rsf-design/src/views/manager/task-item/taskItemTable.columns.js | 109
rsf-design/src/views/orders/wait-pakin-item-log/index.vue | 329
rsf-design/src/views/orders/check-diff/modules/check-diff-detail-drawer.vue | 61
rsf-design/src/api/task-instance-node.js | 69
rsf-design/src/views/manager/wave-rule/index.vue | 248
rsf-design/src/api/bas-station-area.js | 218
rsf-design/src/api/check-out-bound.js | 52
rsf-design/src/views/system/operation-record/operationRecordPage.helpers.js | 123
rsf-design/src/views/basic-info/bas-container/modules/bas-container-areas-editor.vue | 177
rsf-design/src/views/orders/out-stock/outStockPage.helpers.js | 211
rsf-design/src/api/transfer-item.js | 125
rsf-design/src/views/orders/wait-pakin-item-log/waitPakinItemLogPage.helpers.js | 167
rsf-design/src/views/orders/purchase-item/purchaseItemTable.columns.js | 161
rsf-design/src/views/basic-info/device-bind/modules/device-bind-dialog.vue | 235
rsf-design/src/views/orders/wave/waveTable.columns.js | 179
rsf-design/src/views/abnormal/index.vue | 153
rsf-design/src/views/orders/purchase-item/modules/purchase-item-detail-drawer.vue | 72
rsf-design/src/views/system/ai-observe/index.vue | 279
rsf-design/src/views/manager/task-log/taskLogTable.columns.js | 109
rsf-design/src/views/manager/loc-revise/modules/loc-revise-dialog.vue | 168
rsf-design/src/views/manager/revise-log/index.vue | 266
rsf-design/src/api/revise-log-item.js | 61
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/StockStatisticController.java | 26
rsf-design/src/views/statistics/out-statistic-item/outStatisticItemPage.helpers.js | 89
rsf-design/src/views/manager/qly-inspect/index.vue | 416
rsf-design/src/views/system/ai-param/modules/ai-param-runtime-summary.vue | 142
rsf-design/src/views/basic-info/warehouse/warehousePage.helpers.js | 182
rsf-design/src/router/adapters/backendMenuAdapter.js | 63
rsf-design/src/views/system/fields/modules/fields-detail-drawer.vue | 42
rsf-design/src/router/routes/staticRoutes.js | 89
rsf-design/src/views/work/out-bound/index.vue | 368
rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-relation-panel.vue | 435
rsf-design/src/api/warehouse-stock.js | 60
rsf-design/src/views/orders/wait-pakin-item/modules/wait-pakin-item-detail-drawer.vue | 102
rsf-design/src/views/orders/wave-item/waveItemPage.helpers.js | 165
rsf-design/src/views/system/host/hostTable.columns.js | 68
rsf-design/src/views/orders/check-item/checkOrderItemTable.columns.js | 80
rsf-design/src/views/system/flow-step-template/flowStepTemplateTable.columns.js | 48
rsf-design/src/views/basic-info/bas-container/index.vue | 400
rsf-design/src/views/basic-info/device-site/modules/device-site-detail-drawer.vue | 62
rsf-design/src/views/manager/revise-log/modules/revise-log-detail-drawer.vue | 74
rsf-design/src/views/system/fields/fieldsPage.helpers.js | 134
rsf-design/src/views/system/flow-step-log/index.vue | 244
rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-mount-dialog.vue | 292
rsf-design/src/views/system/fields/modules/fields-dialog.vue | 166
rsf-design/src/views/manager/loc-dead-report/locDeadReportTable.columns.js | 150
rsf-design/src/views/system/flow-step-log/flowStepLogTable.columns.js | 75
rsf-design/src/views/basic-info/device-site/deviceSitePage.helpers.js | 442
rsf-design/src/views/stock/stock-transfer/index.vue | 312
rsf-design/src/views/system/ai-prompt/aiPromptPage.helpers.js | 182
rsf-design/src/views/orders/purchase-item/index.vue | 370
rsf-design/package.json | 2
rsf-design/src/api/out-stock.js | 75
rsf-design/src/views/basic-info/task-path-template-merge/index.vue | 323
rsf-design/src/router/core/RouteTransformer.js | 2
rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue | 67
rsf-design/src/api/companys.js | 105
rsf-design/src/api/wait-pakin.js | 111
rsf-design/src/plugins/iconify.collections.js | 66
rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue | 73
rsf-design/src/api/preparation.js | 90
rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js | 474
rsf-design/src/api/in-statistic.js | 43
rsf-design/src/views/basic-info/loc-type/locTypeTable.columns.js | 115
rsf-design/src/views/basic-info/task-path-template/taskPathTemplateTable.columns.js | 207
rsf-design/src/views/statistics/in-statistic-item/inStatisticItemTable.columns.js | 128
rsf-design/src/views/basic-info/device-bind/modules/device-bind-detail-drawer.vue | 79
rsf-design/src/views/system/fields-item/fieldsItemPage.helpers.js | 136
rsf-design/src/views/manager/qly-ispt-item/modules/qly-ispt-item-detail-drawer.vue | 49
rsf-design/src/views/orders/wait-pakin-log/modules/wait-pakin-log-detail-drawer.vue | 59
rsf-design/src/views/manager/stock-item/modules/stock-item-dialog.vue | 450
rsf-design/src/views/basic-info/loc-area-mat-rela/locAreaMatRelaTable.columns.js | 138
rsf-design/src/views/basic-info/task-path-template-merge/modules/task-path-template-merge-dialog.vue | 320
rsf-design/src/views/manager/menu-pda/index.vue | 222
rsf-design/src/router/core/RouteValidator.js | 10
rsf-design/src/views/manager/loc-revise/index.vue | 542
rsf-design/src/views/manager/menu-pda/menuPdaPage.helpers.js | 124
rsf-design/src/api/revise-log.js | 61
rsf-design/src/api/qly-inspect.js | 76
rsf-design/src/views/basic-info/task-path-template-node/taskPathTemplateNodePage.helpers.js | 311
rsf-design/src/views/orders/out-stock-item/outStockItemTable.columns.js | 119
rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-detail-drawer.vue | 76
rsf-design/src/views/orders/wait-pakin-log/index.vue | 339
rsf-design/src/views/stock/warehouse-stock/index.vue | 503
rsf-design/src/views/manager/revise-log-item/reviseLogItemPage.helpers.js | 128
rsf-design/src/views/manager/in-statistic-item/modules/in-statistic-item-detail-drawer.vue | 58
rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-tools-drawer.vue | 190
rsf-design/src/api/flow-step-log.js | 69
rsf-design/src/views/manager/loc-item/index.vue | 234
rsf-design/src/views/orders/asn-order/modules/asn-order-create-by-po-dialog.vue | 343
rsf-design/src/views/basic-info/loc-area/locAreaPage.helpers.js | 202
rsf-design/src/views/orders/purchase-item/purchaseItemPage.helpers.js | 183
rsf-design/src/views/manager/wave-rule/waveRulePage.helpers.js | 114
rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue | 78
rsf-design/src/views/manager/loc-item/locItemTable.columns.js | 143
rsf-design/src/views/system/fields-item/fieldsItemTable.columns.js | 80
rsf-design/src/views/manager/task/taskTable.columns.js | 93
rsf-design/src/api/wait-pakin-item.js | 105
rsf-design/src/views/basic-info/loc-area/modules/loc-area-detail-drawer.vue | 56
rsf-design/src/views/manager/stock-item/stockItemPage.helpers.js | 342
rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js | 136
rsf-design/src/views/work/out-bound/modules/out-bound-detail-drawer.vue | 41
rsf-design/src/views/basic-info/contract/contractTable.columns.js | 99
rsf-design/src/views/orders/transfer/transferPage.helpers.js | 347
rsf-design/src/views/orders/asn-order-item-log/index.vue | 256
rsf-design/src/views/orders/delivery/index.vue | 387
rsf-design/src/views/system/role/modules/role-permission-dialog.vue | 17
rsf-design/src/views/basic-info/warehouse/modules/warehouse-dialog.vue | 167
rsf-design/src/views/system/host/modules/host-dialog.vue | 130
rsf-design/src/views/system/menu/index.vue | 323
rsf-design/src/views/system/subsystem-flow-template/index.vue | 185
rsf-design/src/views/basic-info/contract/modules/contract-detail-drawer.vue | 55
rsf-design/src/views/basic-info/task-path-template-merge/taskPathTemplateMergePage.helpers.js | 484
rsf-design/src/api/stock.js | 66
rsf-design/src/views/system/config/configTable.columns.js | 80
rsf-design/src/views/system/tenant/modules/tenant-edit-dialog.vue | 149
rsf-design/src/api/matnr-group.js | 154
rsf-design/src/views/manager/qly-ispt-item/qlyIsptItemPage.helpers.js | 167
rsf-design/src/views/orders/out-stock-item/index.vue | 217
/dev/null | 19
rsf-design/src/views/basic-info/bas-station/basStationTable.columns.js | 174
rsf-design/src/views/system/fields/fieldsTable.columns.js | 73
rsf-design/src/views/orders/delivery-item/index.vue | 178
rsf-design/src/api/dashboard.js | 44
rsf-design/src/api/warehouse.js | 140
rsf-design/src/views/basic-info/loc-area-mat/locAreaMatTable.columns.js | 125
rsf-design/src/views/basic-info/matnr-group/modules/matnr-group-detail-drawer.vue | 58
500 files changed, 82,337 insertions(+), 632 deletions(-)
diff --git a/.tmp-menu.json b/.tmp-menu.json
new file mode 100644
index 0000000..9ee9b13
--- /dev/null
+++ b/.tmp-menu.json
@@ -0,0 +1,931 @@
+{
+ "msg": "Success",
+ "code": 200,
+ "data": [
+ {
+ "id": 5318,
+ "name": "AI绠$悊涓績",
+ "parentId": 0,
+ "path": "",
+ "route": "/AI",
+ "component": null,
+ "type": 0,
+ "icon": "ri:command-fill",
+ "sort": 0,
+ "children": [
+ {
+ "id": 422,
+ "name": "menu.aiParam",
+ "parentId": 5318,
+ "path": "5318",
+ "route": "/system/aiParam",
+ "component": "aiParam",
+ "type": 0,
+ "icon": "ri:command-fill",
+ "sort": 9,
+ "children": null
+ },
+ {
+ "id": 428,
+ "name": "menu.aiPrompt",
+ "parentId": 5318,
+ "path": "5318",
+ "route": "/system/aiPrompt",
+ "component": "aiPrompt",
+ "type": 0,
+ "icon": "ri:function-line",
+ "sort": 10,
+ "children": null
+ },
+ {
+ "id": 430,
+ "name": "menu.aiCallLog",
+ "parentId": 5318,
+ "path": "5318",
+ "route": "/system/aiCallLog",
+ "component": "aiCallLog",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 12,
+ "children": null
+ },
+ {
+ "id": 5311,
+ "name": "menu.aiMcpMount",
+ "parentId": 5318,
+ "path": "5318",
+ "route": "/system/aiMcpMount",
+ "component": "aiMcpMount",
+ "type": 0,
+ "icon": "ri:command-fill",
+ "sort": 13,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 67,
+ "name": "menu.basicInfo",
+ "parentId": 0,
+ "path": "",
+ "route": "/basic",
+ "component": null,
+ "type": 0,
+ "icon": "ri:home-smile-2-line",
+ "sort": 1,
+ "children": [
+ {
+ "id": 417,
+ "name": "menu.basStationArea",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/basStationArea",
+ "component": "basStationArea",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 0,
+ "children": null
+ },
+ {
+ "id": 68,
+ "name": "menu.warehouse",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/warehouse",
+ "component": "warehouse",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 1,
+ "children": null
+ },
+ {
+ "id": 73,
+ "name": "menu.warehouseAreas",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/warehouseAreas",
+ "component": "warehouseAreas",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 2,
+ "children": null
+ },
+ {
+ "id": 78,
+ "name": "menu.loc",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/loc",
+ "component": "loc",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 3,
+ "children": null
+ },
+ {
+ "id": 62,
+ "name": "menu.matnrGroup",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/matnrGroup",
+ "component": "matnrGroup",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 4,
+ "children": null
+ },
+ {
+ "id": 57,
+ "name": "menu.matnr",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/matnr",
+ "component": "matnr",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 5,
+ "children": null
+ },
+ {
+ "id": 331,
+ "name": "menu.basContainer",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/basContainer",
+ "component": "basContainer",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 6,
+ "children": null
+ },
+ {
+ "id": 325,
+ "name": "menu.basStation",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/basStation",
+ "component": "basStation",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 7,
+ "children": null
+ },
+ {
+ "id": 302,
+ "name": "menu.deviceBind",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/deviceBind",
+ "component": "deviceBind",
+ "type": 0,
+ "icon": "ri:command-fill",
+ "sort": 8,
+ "children": null
+ },
+ {
+ "id": 200,
+ "name": "menu.deviceSite",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/deviceSite",
+ "component": "deviceSite",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 9,
+ "children": null
+ },
+ {
+ "id": 118,
+ "name": "menu.companys",
+ "parentId": 67,
+ "path": "67",
+ "route": "/manager/companys",
+ "component": "companys",
+ "type": 0,
+ "icon": "ri:group-line",
+ "sort": 10,
+ "children": null
+ },
+ {
+ "id": 405,
+ "name": "menu.taskPathTemplate",
+ "parentId": 67,
+ "path": "67",
+ "route": "/taskPathTemplate",
+ "component": "taskPathTemplate",
+ "type": 0,
+ "icon": "ri:book-2-line",
+ "sort": 11,
+ "children": null
+ },
+ {
+ "id": 410,
+ "name": "menu.taskPathTemplateMerge",
+ "parentId": 67,
+ "path": "67",
+ "route": "/taskPathTemplateMerge",
+ "component": "taskPathTemplateMerge",
+ "type": 0,
+ "icon": "ri:book-2-line",
+ "sort": 12,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 300,
+ "name": "menu.inStockPoces",
+ "parentId": 0,
+ "path": "",
+ "route": "/stock/in",
+ "component": null,
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 2,
+ "children": [
+ {
+ "id": 134,
+ "name": "menu.asnOrder",
+ "parentId": 300,
+ "path": "300",
+ "route": "/manager/asnOrder",
+ "component": "asnOrder",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 2,
+ "children": null
+ },
+ {
+ "id": 400,
+ "name": "鍏ュ簱閫氱煡鍗曟槑缁�",
+ "parentId": 300,
+ "path": "300",
+ "route": "/manager/asnOrderItem",
+ "component": "asnOrderItem",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 3,
+ "children": null
+ },
+ {
+ "id": 216,
+ "name": "menu.asnOrderLog",
+ "parentId": 300,
+ "path": "300",
+ "route": "/manager/asnOrderLog",
+ "component": "asnOrderLog",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 4,
+ "children": null
+ },
+ {
+ "id": 205,
+ "name": "menu.waitPakin",
+ "parentId": 300,
+ "path": "300",
+ "route": "/manager/waitPakin",
+ "component": "waitPakin",
+ "type": 0,
+ "icon": "ri:function-line",
+ "sort": 5,
+ "children": null
+ },
+ {
+ "id": 246,
+ "name": "menu.waitPakinLog",
+ "parentId": 300,
+ "path": "300",
+ "route": "/manager/waitPakinLog",
+ "component": "waitPakinLog",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 6,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 301,
+ "name": "menu.outStockPoces",
+ "parentId": 0,
+ "path": "",
+ "route": "/stock/out",
+ "component": null,
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 3,
+ "children": [
+ {
+ "id": 290,
+ "name": "menu.outStock",
+ "parentId": 301,
+ "path": "301",
+ "route": "manager/outStock",
+ "component": "outStock",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 2,
+ "children": null
+ },
+ {
+ "id": 402,
+ "name": "鍑哄簱璁㈠崟鏄庣粏",
+ "parentId": 301,
+ "path": "301",
+ "route": "/manager/outStockItem",
+ "component": "outStockItem",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 3,
+ "children": null
+ },
+ {
+ "id": 387,
+ "name": "menu.preparation",
+ "parentId": 301,
+ "path": "301",
+ "route": "/manager/matPreparation",
+ "component": "preparation",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 4,
+ "children": null
+ },
+ {
+ "id": 307,
+ "name": "menu.wave",
+ "parentId": 301,
+ "path": "301",
+ "route": "/manager/wave",
+ "component": "wave",
+ "type": 0,
+ "icon": "ri:progress-2-line",
+ "sort": 6,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 389,
+ "name": "menu.transferPoces",
+ "parentId": 0,
+ "path": "",
+ "route": "/transfer",
+ "component": "",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 4,
+ "children": [
+ {
+ "id": 356,
+ "name": "menu.transfer",
+ "parentId": 389,
+ "path": "389",
+ "route": "/manager/transfer",
+ "component": "transfer",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 0,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 322,
+ "name": "menu.tasks",
+ "parentId": 0,
+ "path": "",
+ "route": "/tasks",
+ "component": null,
+ "type": 0,
+ "icon": "ri:progress-2-line",
+ "sort": 5,
+ "children": [
+ {
+ "id": 336,
+ "name": "menu.outBound",
+ "parentId": 322,
+ "path": "322",
+ "route": "/manager/outBound",
+ "component": "outBound",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 1,
+ "children": null
+ },
+ {
+ "id": 338,
+ "name": "menu.stockTransfer",
+ "parentId": 322,
+ "path": "322",
+ "route": "/manager/stockTransfer",
+ "component": "stockTransfer",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 3,
+ "children": null
+ },
+ {
+ "id": 226,
+ "name": "menu.task",
+ "parentId": 322,
+ "path": "322",
+ "route": "/manager/task",
+ "component": "task",
+ "type": 0,
+ "icon": "ri:progress-2-line",
+ "sort": 5,
+ "children": null
+ },
+ {
+ "id": 236,
+ "name": "menu.taskLog",
+ "parentId": 322,
+ "path": "322",
+ "route": "/manager/taskLog",
+ "component": "taskLog",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 7,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 323,
+ "name": "menu.stockManage",
+ "parentId": 0,
+ "path": "",
+ "route": "/stock/manage",
+ "component": "manage",
+ "type": 0,
+ "icon": "ri:function-line",
+ "sort": 6,
+ "children": [
+ {
+ "id": 330,
+ "name": "menu.warehouseStock",
+ "parentId": 323,
+ "path": "323",
+ "route": "/warehouse/stock",
+ "component": "warehouseStock",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 1,
+ "children": null
+ },
+ {
+ "id": 361,
+ "name": "menu.locPreview",
+ "parentId": 323,
+ "path": "323",
+ "route": "/manager/locPreview",
+ "component": "locPreview",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 2,
+ "children": null
+ },
+ {
+ "id": 172,
+ "name": "menu.warehouseAreasItem",
+ "parentId": 323,
+ "path": "323",
+ "route": "/manager/warehouseAreasItem",
+ "component": "warehouseAreasItem",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 3,
+ "children": null
+ },
+ {
+ "id": 103,
+ "name": "menu.qlyInspect",
+ "parentId": 323,
+ "path": "323",
+ "route": "/manager/qlyInspect",
+ "component": "qlyInspect",
+ "type": 0,
+ "icon": "ri:function-line",
+ "sort": 4,
+ "children": null
+ },
+ {
+ "id": 366,
+ "name": "menu.locRevise",
+ "parentId": 323,
+ "path": "323",
+ "route": "/manager/locRevise",
+ "component": "locRevise",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 10,
+ "children": null
+ },
+ {
+ "id": 394,
+ "name": "menu.freeze",
+ "parentId": 323,
+ "path": "323",
+ "route": "/manager/freeze",
+ "component": "freeze",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 11,
+ "children": null
+ },
+ {
+ "id": 265,
+ "name": "menu.stock",
+ "parentId": 323,
+ "path": "323",
+ "route": "/manager/stock",
+ "component": "stock",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 12,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 391,
+ "name": "menu.check",
+ "parentId": 0,
+ "path": "",
+ "route": "/check",
+ "component": "check",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 7,
+ "children": [
+ {
+ "id": 345,
+ "name": "menu.checkOrder",
+ "parentId": 391,
+ "path": "391",
+ "route": "/orders/check",
+ "component": "check",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 1,
+ "children": null
+ },
+ {
+ "id": 351,
+ "name": "menu.checkDiff",
+ "parentId": 391,
+ "path": "391",
+ "route": "/manager/checkDiff",
+ "component": "checkDiff",
+ "type": 0,
+ "icon": "ri:error-warning-line",
+ "sort": 2,
+ "children": null
+ },
+ {
+ "id": 337,
+ "name": "menu.checkOutBound",
+ "parentId": 391,
+ "path": "391",
+ "route": "/manager/checkOutBound",
+ "component": "checkOutBound",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 3,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 392,
+ "name": "menu.abnormal",
+ "parentId": 0,
+ "path": "",
+ "route": "/abnormal",
+ "component": "abnormal",
+ "type": 0,
+ "icon": "ri:error-warning-line",
+ "sort": 8,
+ "children": null
+ },
+ {
+ "id": 371,
+ "name": "menu.statisticReport",
+ "parentId": 0,
+ "path": "",
+ "route": "/manager/statisticReport",
+ "component": "",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 9,
+ "children": [
+ {
+ "id": 385,
+ "name": "menu.outStatisticItem",
+ "parentId": 371,
+ "path": "371",
+ "route": "/manager/outStatisticItem",
+ "component": "outStatisticItem",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 0,
+ "children": null
+ },
+ {
+ "id": 386,
+ "name": "menu.statisticCount",
+ "parentId": 371,
+ "path": "371",
+ "route": "/manager/statisticCount",
+ "component": "statisticCount",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 0,
+ "children": null
+ },
+ {
+ "id": 382,
+ "name": "menu.outStatistic",
+ "parentId": 371,
+ "path": "371",
+ "route": "/manager/outStatistic",
+ "component": "outStatistic",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 0,
+ "children": null
+ },
+ {
+ "id": 384,
+ "name": "menu.inStatisticItem",
+ "parentId": 371,
+ "path": "371",
+ "route": "/manager/inStatisticItem",
+ "component": "inStatisticItem",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 0,
+ "children": null
+ },
+ {
+ "id": 383,
+ "name": "menu.inStatistic",
+ "parentId": 371,
+ "path": "371",
+ "route": "/manager/inStatistic",
+ "component": "inStatistic",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 0,
+ "children": null
+ },
+ {
+ "id": 376,
+ "name": "menu.locDeadReport",
+ "parentId": 371,
+ "path": "371",
+ "route": "/manager/locDeadReport",
+ "component": "locDeadReport",
+ "type": 0,
+ "icon": "ri:pie-chart-line",
+ "sort": 1,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 160,
+ "name": "menu.logs",
+ "parentId": 0,
+ "path": "",
+ "route": "/logs",
+ "component": null,
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 10,
+ "children": [
+ {
+ "id": 32,
+ "name": "menu.operation",
+ "parentId": 160,
+ "path": "160",
+ "route": "/system/operationRecord",
+ "component": "operationRecord",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 5,
+ "children": null
+ },
+ {
+ "id": 27,
+ "name": "menu.token",
+ "parentId": 160,
+ "path": "160",
+ "route": "/system/userLogin",
+ "component": "userLogin",
+ "type": 0,
+ "icon": "ri:bill-line",
+ "sort": 6,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 179,
+ "name": "menu.permissions",
+ "parentId": 0,
+ "path": "",
+ "route": "/permissions",
+ "component": null,
+ "type": 0,
+ "icon": "ri:user-settings-line",
+ "sort": 11,
+ "children": [
+ {
+ "id": 2,
+ "name": "menu.user",
+ "parentId": 179,
+ "path": "179",
+ "route": "/system/user",
+ "component": "user",
+ "type": 0,
+ "icon": "ri:user-3-line",
+ "sort": 1,
+ "children": null
+ },
+ {
+ "id": 7,
+ "name": "menu.role",
+ "parentId": 179,
+ "path": "179",
+ "route": "/system/role",
+ "component": "role",
+ "type": 0,
+ "icon": "ri:palette-line",
+ "sort": 2,
+ "children": null
+ },
+ {
+ "id": 22,
+ "name": "menu.department",
+ "parentId": 179,
+ "path": "179",
+ "route": "/system/dept",
+ "component": "dept",
+ "type": 0,
+ "icon": "ri:group-line",
+ "sort": 4,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 1,
+ "name": "menu.system",
+ "parentId": 0,
+ "path": "",
+ "route": "/system",
+ "component": null,
+ "type": 0,
+ "icon": "ri:settings-line",
+ "sort": 12,
+ "children": [
+ {
+ "id": 12,
+ "name": "menu.menu",
+ "parentId": 1,
+ "path": "1",
+ "route": "/system/menu",
+ "component": "menu",
+ "type": 0,
+ "icon": "ri:menu-2-fill",
+ "sort": 1,
+ "children": null
+ },
+ {
+ "id": 395,
+ "name": "menu.menuPda",
+ "parentId": 1,
+ "path": "1",
+ "route": "/manager/menuPda",
+ "component": "menuPda",
+ "type": 0,
+ "icon": "ri:menu-2-fill",
+ "sort": 2,
+ "children": null
+ },
+ {
+ "id": 123,
+ "name": "menu.serialRule",
+ "parentId": 1,
+ "path": "1",
+ "route": "/system/serialRule",
+ "component": "serialRule",
+ "type": 0,
+ "icon": "ri:function-line",
+ "sort": 3,
+ "children": null
+ },
+ {
+ "id": 340,
+ "name": "menu.waveRule",
+ "parentId": 1,
+ "path": "1",
+ "route": "/manager/waveRule",
+ "component": "waveRule",
+ "type": 0,
+ "icon": "ri:progress-2-line",
+ "sort": 4,
+ "children": null
+ },
+ {
+ "id": 108,
+ "name": "menu.dictType",
+ "parentId": 1,
+ "path": "1",
+ "route": "/system/dictType",
+ "component": "dictType",
+ "type": 0,
+ "icon": "ri:book-2-line",
+ "sort": 5,
+ "children": null
+ },
+ {
+ "id": 37,
+ "name": "menu.config",
+ "parentId": 1,
+ "path": "1",
+ "route": "/system/config",
+ "component": "config",
+ "type": 0,
+ "icon": "ri:book-2-line",
+ "sort": 7,
+ "children": null
+ },
+ {
+ "id": 162,
+ "name": "menu.fields",
+ "parentId": 1,
+ "path": "1",
+ "route": "/system/fields",
+ "component": "fields",
+ "type": 0,
+ "icon": "ri:book-2-line",
+ "sort": 10,
+ "children": null
+ },
+ {
+ "id": 167,
+ "name": "menu.fieldsItem",
+ "parentId": 1,
+ "path": "1",
+ "route": "/system/fieldsItem",
+ "component": "fieldsItem",
+ "type": 0,
+ "icon": "ri:book-2-line",
+ "sort": 11,
+ "children": null
+ }
+ ]
+ },
+ {
+ "id": 390,
+ "name": "menu.platform",
+ "parentId": 0,
+ "path": "",
+ "route": "/platform",
+ "component": "platform",
+ "type": 0,
+ "icon": "ri:command-fill",
+ "sort": 13,
+ "children": [
+ {
+ "id": 42,
+ "name": "menu.tenant",
+ "parentId": 390,
+ "path": "390",
+ "route": "/system/tenant",
+ "component": "tenant",
+ "type": 0,
+ "icon": "ri:command-fill",
+ "sort": 8,
+ "children": null
+ },
+ {
+ "id": 17,
+ "name": "menu.host",
+ "parentId": 390,
+ "path": "390",
+ "route": "/system/host",
+ "component": "host",
+ "type": 0,
+ "icon": "ri:map-pin-line",
+ "sort": 9,
+ "children": null
+ }
+ ]
+ }
+ ]
+}
diff --git a/rsf-design/package.json b/rsf-design/package.json
index f0db9dd..82a0385 100644
--- a/rsf-design/package.json
+++ b/rsf-design/package.json
@@ -7,7 +7,7 @@
"pnpm": ">=8.8.0"
},
"scripts": {
- "test": "node --test tests/auth-contract.test.mjs tests/backend-menu-adapter.test.mjs tests/clean-dev-helpers.test.mjs tests/iconify-local-minimal.test.mjs tests/iconify-local-prefixes.test.mjs tests/manual-chunks.test.mjs tests/repo-hygiene.test.mjs tests/system-manage-contract.test.mjs tests/system-role-scope-contract.test.mjs tests/system-user-page-contract.test.mjs tests/work-tab-icon-contract.test.mjs tests/worktab-icon-normalization-contract.test.mjs",
+ "test": "node --test tests/auth-contract.test.mjs tests/backend-menu-adapter.test.mjs tests/clean-dev-helpers.test.mjs tests/iconify-local-minimal.test.mjs tests/iconify-local-prefixes.test.mjs tests/manual-chunks.test.mjs tests/orders-out-stock-item-page-contract.test.mjs tests/repo-hygiene.test.mjs tests/system-manage-contract.test.mjs tests/system-role-scope-contract.test.mjs tests/system-user-page-contract.test.mjs tests/work-tab-icon-contract.test.mjs tests/worktab-icon-normalization-contract.test.mjs",
"dev": "vite --open",
"build": "vite build",
"serve": "vite preview",
diff --git a/rsf-design/src/api/ai-config.js b/rsf-design/src/api/ai-config.js
new file mode 100644
index 0000000..909bb24
--- /dev/null
+++ b/rsf-design/src/api/ai-config.js
@@ -0,0 +1,225 @@
+import request from '@/utils/http'
+
+export function buildAiParamPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.providerType !== undefined ? { providerType: params.providerType } : {}),
+ ...(params.model !== undefined ? { model: params.model } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {})
+ }
+}
+
+function normalizeManyIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id ?? '').trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ return String(ids ?? '').trim()
+}
+
+function fetchAiParamPage(params = {}) {
+ return request.post({ url: '/aiParam/page', params: buildAiParamPageParams(params) })
+}
+
+function fetchGetAiParamDetail(id) {
+ return request.get({ url: `/aiParam/${id}` })
+}
+
+function fetchGetAiParamMany(ids) {
+ return request.post({ url: `/aiParam/many/${normalizeManyIds(ids)}` })
+}
+
+function fetchSaveAiParam(params) {
+ return request.post({ url: '/aiParam/save', params })
+}
+
+function fetchUpdateAiParam(params) {
+ return request.post({ url: '/aiParam/update', params })
+}
+
+function fetchDeleteAiParam(id) {
+ return request.post({ url: `/aiParam/remove/${id}` })
+}
+
+function fetchValidateAiParamDraft(params) {
+ return request.post({ url: '/aiParam/validate-draft', params })
+}
+
+function fetchSetAiParamDefault(id) {
+ return request.post({ url: `/aiParam/set-default/${id}` })
+}
+
+function fetchGetAiConfigSummary(promptCode) {
+ return request.get({
+ url: '/ai/config/summary',
+ params: promptCode ? { promptCode } : {}
+ })
+}
+
+async function fetchExportAiParamReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/aiParam/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
+
+export function buildAiPromptPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.code !== undefined ? { code: params.code } : {}),
+ ...(params.scene !== undefined ? { scene: params.scene } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {})
+ }
+}
+
+function fetchAiPromptPage(params = {}) {
+ return request.post({ url: '/aiPrompt/page', params: buildAiPromptPageParams(params) })
+}
+
+function fetchGetAiPromptDetail(id) {
+ return request.get({ url: `/aiPrompt/${id}` })
+}
+
+function fetchSaveAiPrompt(params) {
+ return request.post({ url: '/aiPrompt/save', params })
+}
+
+function fetchUpdateAiPrompt(params) {
+ return request.post({ url: '/aiPrompt/update', params })
+}
+
+function fetchDeleteAiPrompt(id) {
+ return request.post({ url: `/aiPrompt/remove/${id}` })
+}
+
+function fetchRenderAiPromptPreview(params) {
+ return request.post({ url: '/aiPrompt/render-preview', params })
+}
+
+async function fetchExportAiPromptReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/aiPrompt/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
+
+export function buildAiObservePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.requestId !== undefined ? { requestId: params.requestId } : {}),
+ ...(params.promptCode !== undefined ? { promptCode: params.promptCode } : {}),
+ ...(params.userId !== undefined ? { userId: params.userId } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {})
+ }
+}
+
+function fetchAiCallLogPage(params = {}) {
+ return request.post({ url: '/aiCallLog/page', params: buildAiObservePageParams(params) })
+}
+
+function fetchGetAiCallLogDetail(id) {
+ return request.get({ url: `/aiCallLog/${id}` })
+}
+
+function fetchGetAiObserveStats() {
+ return request.get({ url: '/aiCallLog/stats' })
+}
+
+function fetchGetAiCallLogMcpLogs(id) {
+ return request.get({ url: `/aiCallLog/${id}/mcpLogs` })
+}
+
+export function buildAiMcpMountPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.transportType !== undefined ? { transportType: params.transportType } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {})
+ }
+}
+
+function fetchAiMcpMountPage(params = {}) {
+ return request.post({ url: '/aiMcpMount/page', params: buildAiMcpMountPageParams(params) })
+}
+
+function fetchGetAiMcpMountDetail(id) {
+ return request.get({ url: `/aiMcpMount/${id}` })
+}
+
+function fetchSaveAiMcpMount(params) {
+ return request.post({ url: '/aiMcpMount/save', params })
+}
+
+function fetchUpdateAiMcpMount(params) {
+ return request.post({ url: '/aiMcpMount/update', params })
+}
+
+function fetchDeleteAiMcpMount(id) {
+ return request.post({ url: `/aiMcpMount/remove/${id}` })
+}
+
+function fetchPreviewAiMcpTools(id) {
+ return request.get({ url: `/aiMcpMount/${id}/tools` })
+}
+
+function fetchTestAiMcpConnectivity(id) {
+ return request.post({ url: `/aiMcpMount/${id}/connectivity/test` })
+}
+
+function fetchValidateAiMcpDraftConnectivity(params) {
+ return request.post({ url: '/aiMcpMount/connectivity/validate-draft', params })
+}
+
+function fetchTestAiMcpTool(id, params) {
+ return request.post({ url: `/aiMcpMount/${id}/tool/test`, params })
+}
+
+export {
+ fetchAiCallLogPage,
+ fetchAiMcpMountPage,
+ fetchAiParamPage,
+ fetchAiPromptPage,
+ fetchDeleteAiMcpMount,
+ fetchDeleteAiParam,
+ fetchDeleteAiPrompt,
+ fetchExportAiParamReport,
+ fetchExportAiPromptReport,
+ fetchGetAiCallLogDetail,
+ fetchGetAiCallLogMcpLogs,
+ fetchGetAiConfigSummary,
+ fetchGetAiMcpMountDetail,
+ fetchGetAiObserveStats,
+ fetchGetAiParamDetail,
+ fetchGetAiParamMany,
+ fetchGetAiPromptDetail,
+ fetchPreviewAiMcpTools,
+ fetchSaveAiMcpMount,
+ fetchSaveAiParam,
+ fetchRenderAiPromptPreview,
+ fetchSaveAiPrompt,
+ fetchSetAiParamDefault,
+ fetchTestAiMcpConnectivity,
+ fetchTestAiMcpTool,
+ fetchUpdateAiMcpMount,
+ fetchUpdateAiParam,
+ fetchUpdateAiPrompt,
+ fetchValidateAiMcpDraftConnectivity,
+ fetchValidateAiParamDraft
+}
diff --git a/rsf-design/src/api/asn-order-item.js b/rsf-design/src/api/asn-order-item.js
new file mode 100644
index 0000000..bfe089b
--- /dev/null
+++ b/rsf-design/src/api/asn-order-item.js
@@ -0,0 +1,78 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ if (Array.isArray(value) && value.length === 0) return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildAsnOrderItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.orderBy ? { orderBy: normalizeText(params.orderBy) } : {}),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'orderBy'])
+ }
+}
+
+export function fetchAsnOrderItemPage(params = {}) {
+ return request.post({
+ url: '/asnOrderItem/page',
+ params: buildAsnOrderItemPageParams(params)
+ })
+}
+
+export function fetchAsnOrderItemFullPage(params = {}) {
+ return request.post({
+ url: '/asnOrderItemFull/in/page',
+ params: buildAsnOrderItemPageParams(params)
+ })
+}
+
+export function fetchGetAsnOrderItemDetail(id) {
+ return request.get({
+ url: `/asnOrderItem/${id}`
+ })
+}
+
+export function fetchGetAsnOrderItemMany(ids) {
+ return request.post({
+ url: `/asnOrderItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportAsnOrderItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/asnOrderItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/asn-order-log.js b/rsf-design/src/api/asn-order-log.js
new file mode 100644
index 0000000..e0d5bff
--- /dev/null
+++ b/rsf-design/src/api/asn-order-log.js
@@ -0,0 +1,108 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildAsnOrderLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildAsnOrderItemLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.logId !== undefined ? { logId: Number(params.logId) } : {}),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'logId'])
+ }
+}
+
+export function fetchAsnOrderLogPage(params = {}) {
+ return request.post({
+ url: '/asnOrderLog/page',
+ params: buildAsnOrderLogPageParams(params)
+ })
+}
+
+export function fetchGetAsnOrderLogDetail(id) {
+ return request.get({
+ url: `/asnOrderLog/${id}`
+ })
+}
+
+export function fetchGetAsnOrderLogMany(ids) {
+ return request.post({
+ url: `/asnOrderLog/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchAsnOrderItemLogPage(params = {}) {
+ return request.post({
+ url: '/asnOrderItemLog/page',
+ params: buildAsnOrderItemLogPageParams(params)
+ })
+}
+
+export function fetchGetAsnOrderItemLogDetail(id) {
+ return request.get({
+ url: `/asnOrderItemLog/${id}`
+ })
+}
+
+export function fetchGetAsnOrderItemLogMany(ids) {
+ return request.post({
+ url: `/asnOrderItemLog/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportAsnOrderItemLogReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/asnOrderItemLog/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
+
+export async function fetchExportAsnOrderLogReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/asnOrderLog/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/asn-order.js b/rsf-design/src/api/asn-order.js
new file mode 100644
index 0000000..dc822da
--- /dev/null
+++ b/rsf-design/src/api/asn-order.js
@@ -0,0 +1,129 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildAsnOrderPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildAsnOrderItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.orderId !== undefined ? { orderId: Number(params.orderId) } : {}),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'orderId'])
+ }
+}
+
+export function buildPurchaseFilterPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildPurchaseItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.purchaseId !== undefined ? { purchaseId: Number(params.purchaseId) } : {}),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'purchaseId'])
+ }
+}
+
+export function fetchAsnOrderPage(params = {}) {
+ return request.post({
+ url: '/asnOrder/page',
+ params: buildAsnOrderPageParams(params)
+ })
+}
+
+export function fetchGetAsnOrderDetail(id) {
+ return request.get({
+ url: `/asnOrder/${id}`
+ })
+}
+
+export function fetchGetAsnOrderMany(ids) {
+ return request.post({
+ url: `/asnOrder/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchAsnOrderItemPage(params = {}) {
+ return request.post({
+ url: '/asnOrderItem/page',
+ params: buildAsnOrderItemPageParams(params)
+ })
+}
+
+export function fetchCompleteAsnOrder(id) {
+ return request.post({
+ url: `/asnOrder/complete/${id}`
+ })
+}
+
+export function fetchPurchaseFilterPage(params = {}) {
+ return request.post({
+ url: '/purchase/filters/page',
+ params: buildPurchaseFilterPageParams(params)
+ })
+}
+
+export function fetchPurchaseItemPage(params = {}) {
+ return request.post({
+ url: '/purchaseItem/page',
+ params: buildPurchaseItemPageParams(params)
+ })
+}
+
+export function fetchCreateAsnOrderByPurchase(payload = {}) {
+ return request.post({
+ url: '/asnOrder/purchases/save',
+ params: payload
+ })
+}
+
+export async function fetchExportAsnOrderReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/asnOrder/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/bas-container.js b/rsf-design/src/api/bas-container.js
new file mode 100644
index 0000000..f673660
--- /dev/null
+++ b/rsf-design/src/api/bas-container.js
@@ -0,0 +1,199 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildBasContainerPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildBasContainerSavePayload(formData = {}) {
+ const areas = normalizeBasContainerAreas(formData.areas)
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ containerType:
+ formData.containerType !== undefined && formData.containerType !== null && formData.containerType !== ''
+ ? Number(formData.containerType)
+ : void 0,
+ codeType: normalizeText(formData.codeType) || '',
+ ...(areas.length ? { areas } : {}),
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function normalizeBasContainerAreas(areas = []) {
+ if (!Array.isArray(areas)) {
+ return []
+ }
+
+ return areas
+ .map((item, index) => {
+ if (item === null || item === undefined) {
+ return null
+ }
+
+ if (typeof item === 'object') {
+ const id = normalizeNumberValue(item.id ?? item.areaId ?? item.value)
+ if (id === void 0) {
+ return null
+ }
+ const sort = normalizeSortValue(item.sort, index + 1)
+ return { id, sort }
+ }
+
+ const id = normalizeNumberValue(item)
+ if (id === void 0) {
+ return null
+ }
+ return { id, sort: index + 1 }
+ })
+ .filter(Boolean)
+ .sort((left, right) => {
+ const leftSort = normalizeSortValue(left.sort, Number.MAX_SAFE_INTEGER)
+ const rightSort = normalizeSortValue(right.sort, Number.MAX_SAFE_INTEGER)
+ return leftSort - rightSort
+ })
+ .map((item, index) => ({
+ id: item.id,
+ sort: index + 1
+ }))
+}
+
+function normalizeNumberValue(value) {
+ if (value === '' || value === null || value === undefined) {
+ return void 0
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? void 0 : numberValue
+}
+
+function normalizeSortValue(value, fallback = 1) {
+ const numberValue = normalizeNumberValue(value)
+ return numberValue === void 0 ? fallback : numberValue
+}
+
+export function buildBasContainerSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ containerType:
+ params.containerType !== undefined && params.containerType !== null && params.containerType !== ''
+ ? Number(params.containerType)
+ : void 0,
+ codeType: normalizeText(params.codeType),
+ areas: normalizeText(params.areas),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function fetchBasContainerPage(params = {}) {
+ return request.post({
+ url: '/basContainer/page',
+ params: buildBasContainerPageParams(params)
+ })
+}
+
+export function fetchGetBasContainerDetail(id) {
+ return request.get({
+ url: `/basContainer/${id}`
+ })
+}
+
+export function fetchGetBasContainerMany(ids) {
+ return request.post({
+ url: `/basContainer/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveBasContainer(params = {}) {
+ return request.post({
+ url: '/basContainer/save',
+ params: buildBasContainerSavePayload(params)
+ })
+}
+
+export function fetchUpdateBasContainer(params = {}) {
+ return request.post({
+ url: '/basContainer/update',
+ params: buildBasContainerSavePayload(params)
+ })
+}
+
+export function fetchDeleteBasContainer(ids) {
+ return request.post({
+ url: `/basContainer/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchBasContainerQuery(condition = '') {
+ return request.post({
+ url: '/basContainer/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportBasContainerReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/basContainer/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
+
+export function fetchWarehouseAreasList() {
+ return request.post({
+ url: '/warehouseAreas/list',
+ data: {}
+ })
+}
diff --git a/rsf-design/src/api/bas-station-area.js b/rsf-design/src/api/bas-station-area.js
new file mode 100644
index 0000000..7919a1a
--- /dev/null
+++ b/rsf-design/src/api/bas-station-area.js
@@ -0,0 +1,218 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildBasStationAreaPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildBasStationAreaSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ stationAreaName: normalizeText(params.stationAreaName),
+ stationAreaId: normalizeText(params.stationAreaId),
+ type:
+ params.type !== undefined && params.type !== null && params.type !== ''
+ ? Number(params.type)
+ : void 0,
+ inAble:
+ params.inAble !== undefined && params.inAble !== null && params.inAble !== ''
+ ? Number(params.inAble)
+ : void 0,
+ outAble:
+ params.outAble !== undefined && params.outAble !== null && params.outAble !== ''
+ ? Number(params.outAble)
+ : void 0,
+ useStatus: normalizeText(params.useStatus),
+ area:
+ params.area !== undefined && params.area !== null && params.area !== ''
+ ? Number(params.area)
+ : void 0,
+ isCrossZone:
+ params.isCrossZone !== undefined && params.isCrossZone !== null && params.isCrossZone !== ''
+ ? Number(params.isCrossZone)
+ : void 0,
+ crossZoneArea: normalizeText(params.crossZoneArea),
+ isWcs:
+ params.isWcs !== undefined && params.isWcs !== null && params.isWcs !== ''
+ ? Number(params.isWcs)
+ : void 0,
+ wcsData: normalizeText(params.wcsData),
+ containerType: normalizeText(params.containerType),
+ barcode: normalizeText(params.barcode),
+ autoTransfer:
+ params.autoTransfer !== undefined && params.autoTransfer !== null && params.autoTransfer !== ''
+ ? Number(params.autoTransfer)
+ : void 0,
+ stationAlias: normalizeText(params.stationAlias),
+ memo: normalizeText(params.memo),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+function normalizeIdArray(ids = []) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((item) => {
+ if (item === null || item === undefined || item === '') {
+ return void 0
+ }
+ const parsed = Number(item)
+ return Number.isNaN(parsed) ? void 0 : parsed
+ })
+ .filter((item) => item !== void 0)
+ }
+
+ if (typeof ids === 'string' && ids.trim()) {
+ return ids
+ .split(',')
+ .map((item) => {
+ const parsed = Number(item.trim())
+ return Number.isNaN(parsed) ? void 0 : parsed
+ })
+ .filter((item) => item !== void 0)
+ }
+
+ return []
+}
+
+export function buildBasStationAreaSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ stationAreaName: normalizeText(formData.stationAreaName) || '',
+ stationAreaId: normalizeText(formData.stationAreaId) || '',
+ ...(formData.type !== void 0 && formData.type !== null && formData.type !== ''
+ ? { type: Number(formData.type) }
+ : {}),
+ ...(formData.inAble !== void 0 && formData.inAble !== null && formData.inAble !== ''
+ ? { inAble: Number(formData.inAble) }
+ : {}),
+ ...(formData.outAble !== void 0 && formData.outAble !== null && formData.outAble !== ''
+ ? { outAble: Number(formData.outAble) }
+ : {}),
+ useStatus: normalizeText(formData.useStatus) || '',
+ ...(formData.area !== void 0 && formData.area !== null && formData.area !== ''
+ ? { area: Number(formData.area) }
+ : {}),
+ ...(formData.isCrossZone !== void 0 && formData.isCrossZone !== null && formData.isCrossZone !== ''
+ ? { isCrossZone: Number(formData.isCrossZone) }
+ : {}),
+ ...(Array.isArray(formData.crossZoneArea) && formData.crossZoneArea.length
+ ? { crossZoneArea: normalizeIdArray(formData.crossZoneArea) }
+ : {}),
+ ...(formData.isWcs !== void 0 && formData.isWcs !== null && formData.isWcs !== ''
+ ? { isWcs: Number(formData.isWcs) }
+ : {}),
+ wcsData: normalizeText(formData.wcsData) || '',
+ ...(Array.isArray(formData.containerType) && formData.containerType.length
+ ? { containerType: normalizeIdArray(formData.containerType) }
+ : {}),
+ barcode: normalizeText(formData.barcode) || '',
+ ...(formData.autoTransfer !== void 0 && formData.autoTransfer !== null && formData.autoTransfer !== ''
+ ? { autoTransfer: Number(formData.autoTransfer) }
+ : {}),
+ ...(Array.isArray(formData.stationAlias) && formData.stationAlias.length
+ ? { stationAlias: formData.stationAlias.map((item) => String(item).trim()).filter(Boolean) }
+ : {}),
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchBasStationAreaPage(params = {}) {
+ return request.post({
+ url: '/basStationArea/page',
+ params: buildBasStationAreaPageParams(params)
+ })
+}
+
+export function fetchBasStationAreaList() {
+ return request.post({
+ url: '/basStationArea/list',
+ data: {}
+ })
+}
+
+export function fetchBasStationAreaDetail(id) {
+ return request.get({
+ url: `/basStationArea/${id}`
+ })
+}
+
+export function fetchBasStationAreaMany(ids) {
+ return request.post({
+ url: `/basStationArea/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveBasStationArea(params = {}) {
+ return request.post({
+ url: '/basStationArea/save',
+ params: buildBasStationAreaSavePayload(params)
+ })
+}
+
+export function fetchUpdateBasStationArea(params = {}) {
+ return request.post({
+ url: '/basStationArea/update',
+ params: buildBasStationAreaSavePayload(params)
+ })
+}
+
+export function fetchDeleteBasStationArea(ids) {
+ return request.post({
+ url: `/basStationArea/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchBasStationAreaQuery(condition = '') {
+ return request.post({
+ url: '/basStationArea/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
diff --git a/rsf-design/src/api/bas-station.js b/rsf-design/src/api/bas-station.js
new file mode 100644
index 0000000..bf9bd42
--- /dev/null
+++ b/rsf-design/src/api/bas-station.js
@@ -0,0 +1,190 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildBasStationPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildBasStationSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ stationName: normalizeText(params.stationName),
+ stationId: normalizeText(params.stationId),
+ type:
+ params.type !== undefined && params.type !== null && params.type !== ''
+ ? Number(params.type)
+ : void 0,
+ useStatus: normalizeText(params.useStatus),
+ area:
+ params.area !== undefined && params.area !== null && params.area !== ''
+ ? Number(params.area)
+ : void 0,
+ isCrossZone:
+ params.isCrossZone !== undefined && params.isCrossZone !== null && params.isCrossZone !== ''
+ ? Number(params.isCrossZone)
+ : void 0,
+ isWcs:
+ params.isWcs !== undefined && params.isWcs !== null && params.isWcs !== ''
+ ? Number(params.isWcs)
+ : void 0,
+ barcode: normalizeText(params.barcode),
+ autoTransfer:
+ params.autoTransfer !== undefined && params.autoTransfer !== null && params.autoTransfer !== ''
+ ? Number(params.autoTransfer)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildBasStationSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ stationName: normalizeText(formData.stationName) || '',
+ stationId: normalizeText(formData.stationId) || '',
+ ...(formData.type !== undefined && formData.type !== null && formData.type !== ''
+ ? { type: Number(formData.type) }
+ : {}),
+ ...(formData.area !== undefined && formData.area !== null && formData.area !== ''
+ ? { area: Number(formData.area) }
+ : {}),
+ useStatus: normalizeText(formData.useStatus) || '',
+ ...(formData.isCrossZone !== undefined && formData.isCrossZone !== null && formData.isCrossZone !== ''
+ ? { isCrossZone: Number(formData.isCrossZone) }
+ : {}),
+ ...(Array.isArray(formData.areaIds) && formData.areaIds.length
+ ? { areaIds: formData.areaIds.map((id) => Number(id)).filter((id) => !Number.isNaN(id)) }
+ : {}),
+ ...(formData.isWcs !== undefined && formData.isWcs !== null && formData.isWcs !== ''
+ ? { isWcs: Number(formData.isWcs) }
+ : {}),
+ wcsData: normalizeText(formData.wcsData) || '',
+ ...(Array.isArray(formData.containerTypes) && formData.containerTypes.length
+ ? { containerTypes: formData.containerTypes.map((id) => Number(id)).filter((id) => !Number.isNaN(id)) }
+ : {}),
+ barcode: normalizeText(formData.barcode) || '',
+ ...(formData.autoTransfer !== undefined && formData.autoTransfer !== null && formData.autoTransfer !== ''
+ ? { autoTransfer: Number(formData.autoTransfer) }
+ : {}),
+ ...(formData.inAble !== undefined && formData.inAble !== null && formData.inAble !== ''
+ ? { inAble: Number(formData.inAble) }
+ : {}),
+ ...(formData.outAble !== undefined && formData.outAble !== null && formData.outAble !== ''
+ ? { outAble: Number(formData.outAble) }
+ : {}),
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchBasStationPage(params = {}, requestOptions = {}) {
+ return request.post({
+ url: '/basStation/page',
+ params: buildBasStationPageParams(params),
+ ...requestOptions
+ })
+}
+
+export function fetchBasStationList() {
+ return request.post({
+ url: '/basStation/list',
+ data: {}
+ })
+}
+
+export function fetchGetBasStationDetail(id) {
+ return request.get({
+ url: `/basStation/${id}`
+ })
+}
+
+export function fetchGetBasStationMany(ids) {
+ return request.post({
+ url: `/basStation/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveBasStation(params = {}) {
+ return request.post({
+ url: '/basStation/save',
+ params: buildBasStationSavePayload(params)
+ })
+}
+
+export function fetchUpdateBasStation(params = {}) {
+ return request.post({
+ url: '/basStation/update',
+ params: buildBasStationSavePayload(params)
+ })
+}
+
+export function fetchDeleteBasStation(ids) {
+ return request.post({
+ url: `/basStation/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchBasStationQuery(condition = '') {
+ return request.post({
+ url: '/basStation/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportBasStationReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/basStation/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/check-diff.js b/rsf-design/src/api/check-diff.js
new file mode 100644
index 0000000..9b76461
--- /dev/null
+++ b/rsf-design/src/api/check-diff.js
@@ -0,0 +1,109 @@
+import request from '@/utils/http'
+import {
+ buildCheckDiffItemPageRequestParams,
+ buildCheckDiffPageRequestParams
+} from '../views/orders/check-diff/checkDiffPage.helpers'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((item) => String(item).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildCheckDiffPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildCheckDiffItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchCheckDiffPage(params = {}) {
+ return request.post({ url: '/checkDiff/page', params: buildCheckDiffPageRequestParams(params) })
+}
+
+export function fetchGetCheckDiffDetail(id) {
+ return request.get({ url: `/checkDiff/${id}` })
+}
+
+export function fetchGetCheckDiffMany(ids) {
+ return request.post({ url: `/checkDiff/many/${normalizeIds(ids)}` })
+}
+
+export function fetchCheckDiffItemPage(params = {}) {
+ return request.post({ url: '/checkDiffItem/page', params: buildCheckDiffItemPageRequestParams(params) })
+}
+
+export function fetchGetCheckDiffItemDetail(id) {
+ return request.get({ url: `/checkDiffItem/${id}` })
+}
+
+export function fetchGetCheckDiffItemMany(ids) {
+ return request.post({ url: `/checkDiffItem/many/${normalizeIds(ids)}` })
+}
+
+export function fetchUpdateCheckDiffItem(params = {}) {
+ return request.post({ url: '/checkDiffItem/update', params })
+}
+
+export function fetchDeleteCheckDiff(ids) {
+ return request.post({ url: `/checkDiff/remove/${normalizeIds(ids)}` })
+}
+
+export function fetchDeleteCheckDiffItem(ids) {
+ return request.post({ url: `/checkDiffItem/remove/${normalizeIds(ids)}` })
+}
+
+export async function fetchExportCheckDiffReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/checkDiff/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
+
+export async function fetchExportCheckDiffItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/checkDiffItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/check-order.js b/rsf-design/src/api/check-order.js
new file mode 100644
index 0000000..7a82059
--- /dev/null
+++ b/rsf-design/src/api/check-order.js
@@ -0,0 +1,102 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildCheckOrderPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildCheckOrderItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchCheckOrderPage(params = {}) {
+ return request.post({
+ url: '/check/page',
+ params: buildCheckOrderPageParams(params)
+ })
+}
+
+export function fetchGetCheckOrderDetail(id) {
+ return request.get({
+ url: `/check/${id}`
+ })
+}
+
+export function fetchGetCheckOrderMany(ids) {
+ return request.post({
+ url: `/check/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchCheckOrderItemPage(params = {}) {
+ return request.post({
+ url: '/checkItem/page',
+ params: buildCheckOrderItemPageParams(params)
+ })
+}
+
+export function fetchGetCheckOrderItemDetail(id) {
+ return request.get({
+ url: `/checkItem/${id}`
+ })
+}
+
+export function fetchGetCheckOrderItemMany(ids) {
+ return request.post({
+ url: `/checkItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchCancelCheckOrder(id) {
+ return request.get({
+ url: `/check/cancel/${id}`
+ })
+}
+
+export async function fetchExportCheckOrderReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/check/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/check-out-bound.js b/rsf-design/src/api/check-out-bound.js
new file mode 100644
index 0000000..a1c5733
--- /dev/null
+++ b/rsf-design/src/api/check-out-bound.js
@@ -0,0 +1,52 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildCheckOutBoundPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchCheckOutBoundPage(params = {}) {
+ return request.post({
+ url: '/locItem/useO/page',
+ params: buildCheckOutBoundPageParams(params)
+ })
+}
+
+export function fetchGetCheckOutBoundDetail(id) {
+ return request.get({
+ url: `/locItem/${id}`
+ })
+}
+
+export function fetchCheckOutBoundSites() {
+ return request.get({
+ url: '/check/tasks/sites'
+ })
+}
+
+export function fetchGenerateCheckOutBoundTask(payload = {}) {
+ return request.post({
+ url: '/locItem/check/task',
+ params: payload
+ })
+}
diff --git a/rsf-design/src/api/companys.js b/rsf-design/src/api/companys.js
new file mode 100644
index 0000000..4546a24
--- /dev/null
+++ b/rsf-design/src/api/companys.js
@@ -0,0 +1,105 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildCompanysPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchCompanysPage(params = {}) {
+ return request.post({
+ url: '/companys/page',
+ params: buildCompanysPageParams(params)
+ })
+}
+
+export function fetchCompanysList() {
+ return request.post({
+ url: '/companys/list',
+ data: {}
+ })
+}
+
+export function fetchGetCompanysDetail(id) {
+ return request.get({
+ url: `/companys/${id}`
+ })
+}
+
+export function fetchGetCompanysMany(ids) {
+ return request.post({
+ url: `/companys/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveCompanys(params = {}) {
+ return request.post({
+ url: '/companys/save',
+ params
+ })
+}
+
+export function fetchUpdateCompanys(params = {}) {
+ return request.post({
+ url: '/companys/update',
+ params
+ })
+}
+
+export function fetchDeleteCompanys(ids) {
+ return request.post({
+ url: `/companys/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchCompanysQuery(condition = '') {
+ return request.post({
+ url: '/companys/query',
+ params: {
+ condition: normalizeText(condition)
+ }
+ })
+}
+
+export async function fetchExportCompanysReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/companys/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/contract.js b/rsf-design/src/api/contract.js
new file mode 100644
index 0000000..0bc196e
--- /dev/null
+++ b/rsf-design/src/api/contract.js
@@ -0,0 +1,112 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildContractPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildContractSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ name: normalizeText(formData.name) || '',
+ projectName: normalizeText(formData.projectName) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchContractPage(params = {}) {
+ return request.post({
+ url: '/contract/page',
+ params: buildContractPageParams(params)
+ })
+}
+
+export function fetchGetContractDetail(id) {
+ return request.get({
+ url: `/contract/${id}`
+ })
+}
+
+export function fetchGetContractMany(ids) {
+ return request.post({
+ url: `/contract/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveContract(params = {}) {
+ return request.post({
+ url: '/contract/save',
+ params: buildContractSavePayload(params)
+ })
+}
+
+export function fetchUpdateContract(params = {}) {
+ return request.post({
+ url: '/contract/update',
+ params: buildContractSavePayload(params)
+ })
+}
+
+export function fetchDeleteContract(ids) {
+ return request.post({
+ url: `/contract/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchContractQuery(condition = '') {
+ return request.post({
+ url: '/contract/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportContractReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/contract/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/dashboard.js b/rsf-design/src/api/dashboard.js
new file mode 100644
index 0000000..86c5d52
--- /dev/null
+++ b/rsf-design/src/api/dashboard.js
@@ -0,0 +1,44 @@
+import request from '@/utils/http'
+
+function fetchDashboardHeader() {
+ return request.post({
+ url: '/asnOrder/dashbord/header',
+ params: {}
+ })
+}
+
+function fetchDashboardTrend() {
+ return request.post({
+ url: '/asnOrder/stock/trand',
+ params: {}
+ })
+}
+
+function fetchDashboardDeadStock(params) {
+ return request.post({
+ url: '/locItem/page',
+ params
+ })
+}
+
+function fetchDashboardLocUsage() {
+ return request.post({
+ url: '/loc/pie/list',
+ params: {}
+ })
+}
+
+function fetchDashboardTasks(params) {
+ return request.post({
+ url: '/task/page',
+ params
+ })
+}
+
+export {
+ fetchDashboardDeadStock,
+ fetchDashboardHeader,
+ fetchDashboardLocUsage,
+ fetchDashboardTasks,
+ fetchDashboardTrend
+}
diff --git a/rsf-design/src/api/delivery.js b/rsf-design/src/api/delivery.js
new file mode 100644
index 0000000..0b33f8a
--- /dev/null
+++ b/rsf-design/src/api/delivery.js
@@ -0,0 +1,200 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildDeliverySearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'platId', 'type', 'wkType', 'source', 'timeStart', 'timeEnd', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) result[key] = value
+ })
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ result.status = Number(params.status)
+ }
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = Number(params.exceStatus)
+ }
+ return result
+}
+
+export function buildDeliveryPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildDeliverySearchParams(params)
+ }
+}
+
+export function buildDeliveryDetailQueryParams(params = {}) {
+ return {
+ deliveryId: params.deliveryId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function buildDeliveryItemSearchParams(params = {}) {
+ const result = {}
+ ;[
+ 'condition',
+ 'deliveryCode',
+ 'platItemId',
+ 'matnrCode',
+ 'maktx',
+ 'splrName',
+ 'splrCode',
+ 'splrBatch',
+ 'timeStart',
+ 'timeEnd',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) result[key] = value
+ })
+ if (params.deliveryId !== '' && params.deliveryId !== undefined && params.deliveryId !== null) {
+ result.deliveryId = Number(params.deliveryId)
+ }
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ result.status = Number(params.status)
+ }
+ return result
+}
+
+export function buildDeliveryItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildDeliveryItemSearchParams(params)
+ }
+}
+
+export function buildDeliveryItemDetailQueryParams(params = {}) {
+ return {
+ deliveryItemId: params.deliveryItemId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function buildDeliveryExportParams(payload = {}) {
+ const params = buildDeliverySearchParams(payload)
+ return filterParams(params, ['ids'])
+}
+
+export function fetchDeliveryPage(params = {}) {
+ return request.post({
+ url: '/delivery/page',
+ params: buildDeliveryPageParams(params)
+ })
+}
+
+export function fetchGetDeliveryDetail(id) {
+ return request.get({
+ url: `/delivery/${id}`
+ })
+}
+
+export function fetchGetDeliveryMany(ids) {
+ return request.post({
+ url: `/delivery/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchDeleteDelivery(id) {
+ return request.post({
+ url: `/delivery/remove/${id}`
+ })
+}
+
+export function fetchSaveDelivery(payload = {}) {
+ return request.post({
+ url: '/delivery/save',
+ params: payload
+ })
+}
+
+export function fetchUpdateDelivery(payload = {}) {
+ return request.post({
+ url: '/delivery/update',
+ params: payload
+ })
+}
+
+export function fetchDeliveryItemPage(params = {}) {
+ return request.post({
+ url: '/deliveryItem/page',
+ params: buildDeliveryItemPageParams(params)
+ })
+}
+
+export function fetchGetDeliveryItemDetail(id) {
+ return request.get({
+ url: `/deliveryItem/${id}`
+ })
+}
+
+export function fetchGetDeliveryItemMany(ids) {
+ return request.post({
+ url: `/deliveryItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchDeleteDeliveryItem(id) {
+ return request.post({
+ url: `/deliveryItem/remove/${id}`
+ })
+}
+
+export function fetchSaveDeliveryItem(payload = {}) {
+ return request.post({
+ url: '/deliveryItem/save',
+ params: payload
+ })
+}
+
+export function fetchUpdateDeliveryItem(payload = {}) {
+ return request.post({
+ url: '/deliveryItem/update',
+ params: payload
+ })
+}
+
+export async function fetchExportDeliveryReport(payload = {}, options = {}) {
+ const exportRequest = { url: `/delivery/export` }
+ return fetch(`${import.meta.env.VITE_API_URL}${exportRequest.url}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(buildDeliveryExportParams(payload))
+ })
+}
diff --git a/rsf-design/src/api/device-bind.js b/rsf-design/src/api/device-bind.js
new file mode 100644
index 0000000..81cd505
--- /dev/null
+++ b/rsf-design/src/api/device-bind.js
@@ -0,0 +1,167 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return void 0
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? void 0 : numberValue
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildDeviceBindPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildDeviceBindSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ currentRow: normalizeNumber(params.currentRow),
+ startRow: normalizeNumber(params.startRow),
+ endRow: normalizeNumber(params.endRow),
+ deviceQty: normalizeNumber(params.deviceQty),
+ startDeviceNo: normalizeNumber(params.startDeviceNo),
+ endDeviceNo: normalizeNumber(params.endDeviceNo),
+ typeId: normalizeNumber(params.typeId),
+ staList: normalizeText(params.staList),
+ beSimilar: normalizeText(params.beSimilar),
+ emptySimilar: normalizeText(params.emptySimilar),
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildDeviceBindSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.currentRow !== undefined && formData.currentRow !== null && formData.currentRow !== ''
+ ? { currentRow: Number(formData.currentRow) }
+ : {}),
+ ...(formData.startRow !== undefined && formData.startRow !== null && formData.startRow !== ''
+ ? { startRow: Number(formData.startRow) }
+ : {}),
+ ...(formData.endRow !== undefined && formData.endRow !== null && formData.endRow !== ''
+ ? { endRow: Number(formData.endRow) }
+ : {}),
+ ...(formData.deviceQty !== undefined && formData.deviceQty !== null && formData.deviceQty !== ''
+ ? { deviceQty: Number(formData.deviceQty) }
+ : {}),
+ ...(formData.startDeviceNo !== undefined && formData.startDeviceNo !== null && formData.startDeviceNo !== ''
+ ? { startDeviceNo: Number(formData.startDeviceNo) }
+ : {}),
+ ...(formData.endDeviceNo !== undefined && formData.endDeviceNo !== null && formData.endDeviceNo !== ''
+ ? { endDeviceNo: Number(formData.endDeviceNo) }
+ : {}),
+ staList: normalizeText(formData.staList) || '',
+ ...(formData.typeId !== undefined && formData.typeId !== null && formData.typeId !== ''
+ ? { typeId: Number(formData.typeId) }
+ : {}),
+ beSimilar: normalizeText(formData.beSimilar) || '',
+ emptySimilar: normalizeText(formData.emptySimilar) || '',
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchDeviceBindPage(params = {}) {
+ return request.post({
+ url: '/deviceBind/page',
+ params: buildDeviceBindPageParams(params)
+ })
+}
+
+export function fetchDeviceBindList() {
+ return request.post({
+ url: '/deviceBind/list',
+ data: {}
+ })
+}
+
+export function fetchGetDeviceBindDetail(id) {
+ return request.get({
+ url: `/deviceBind/${id}`
+ })
+}
+
+export function fetchGetDeviceBindMany(ids) {
+ return request.post({
+ url: `/deviceBind/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveDeviceBind(params = {}) {
+ return request.post({
+ url: '/deviceBind/save',
+ params: buildDeviceBindSavePayload(params)
+ })
+}
+
+export function fetchUpdateDeviceBind(params = {}) {
+ return request.post({
+ url: '/deviceBind/update',
+ params: buildDeviceBindSavePayload(params)
+ })
+}
+
+export function fetchDeleteDeviceBind(ids) {
+ return request.post({
+ url: `/deviceBind/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchDeviceBindQuery(condition = '') {
+ return request.post({
+ url: '/deviceBind/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportDeviceBindReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/deviceBind/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/device-site.js b/rsf-design/src/api/device-site.js
new file mode 100644
index 0000000..40d823a
--- /dev/null
+++ b/rsf-design/src/api/device-site.js
@@ -0,0 +1,213 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeMultiValue(value) {
+ if (Array.isArray(value)) {
+ return value.map((item) => normalizeText(item)).filter(Boolean).join(',')
+ }
+ return normalizeText(value)
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildDeviceSitePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildDeviceSiteSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ type: normalizeMultiValue(params.type),
+ site: normalizeText(params.site),
+ name: normalizeText(params.name),
+ target: normalizeText(params.target),
+ label: normalizeText(params.label),
+ device: normalizeText(params.device),
+ deviceCode: normalizeText(params.deviceCode),
+ deviceSite: normalizeText(params.deviceSite),
+ channel: normalizeNumber(params.channel),
+ areaIdStart: normalizeNumber(params.areaIdStart),
+ areaIdEnd: normalizeNumber(params.areaIdEnd),
+ status: normalizeNumber(params.status),
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildDeviceSiteSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ type: normalizeMultiValue(formData.type),
+ site: normalizeText(formData.site) || '',
+ name: normalizeText(formData.name) || '',
+ target: normalizeText(formData.target) || '',
+ label: normalizeText(formData.label) || '',
+ device: normalizeText(formData.device) || '',
+ deviceCode: normalizeText(formData.deviceCode) || '',
+ deviceSite: normalizeText(formData.deviceSite) || '',
+ ...(formData.channel !== void 0 && formData.channel !== null && formData.channel !== ''
+ ? { channel: Number(formData.channel) }
+ : {}),
+ ...(formData.areaIdStart !== void 0 && formData.areaIdStart !== null && formData.areaIdStart !== ''
+ ? { areaIdStart: Number(formData.areaIdStart) }
+ : {}),
+ ...(formData.areaIdEnd !== void 0 && formData.areaIdEnd !== null && formData.areaIdEnd !== ''
+ ? { areaIdEnd: Number(formData.areaIdEnd) }
+ : {}),
+ ...(formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? { status: Number(formData.status) }
+ : {}),
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildDeviceSiteInitPayload(formData = {}) {
+ return {
+ flagInit:
+ formData.flagInit !== void 0 && formData.flagInit !== null && formData.flagInit !== ''
+ ? Number(formData.flagInit)
+ : 0,
+ deviceType: normalizeText(formData.deviceType) || '',
+ typeIds: Array.isArray(formData.typeIds) ? formData.typeIds.map((item) => normalizeText(item)).filter(Boolean) : [],
+ site: normalizeText(formData.site) || '',
+ deviceCode: normalizeText(formData.deviceCode) || '',
+ deviceSites: normalizeText(formData.deviceSites) || '',
+ target: normalizeText(formData.target) || '',
+ rows: Array.isArray(formData.rows)
+ ? formData.rows
+ .map((row) => ({
+ deviceSite: normalizeText(row?.deviceSite) || '',
+ site: normalizeText(row?.site) || '',
+ target: normalizeText(row?.target) || ''
+ }))
+ .filter((row) => row.deviceSite && row.site && row.target)
+ : [],
+ channel: normalizeText(formData.channel) || '',
+ ...(formData.areaIdStart !== void 0 && formData.areaIdStart !== null && formData.areaIdStart !== ''
+ ? { areaIdStart: Number(formData.areaIdStart) }
+ : {}),
+ ...(formData.areaIdEnd !== void 0 && formData.areaIdEnd !== null && formData.areaIdEnd !== ''
+ ? { areaIdEnd: Number(formData.areaIdEnd) }
+ : {}),
+ name: normalizeText(formData.name) || '',
+ wcsCode: normalizeText(formData.wcsCode) || '',
+ label: normalizeText(formData.label) || ''
+ }
+}
+
+export function fetchDeviceSitePage(params = {}) {
+ return request.post({
+ url: '/deviceSite/page',
+ params: buildDeviceSitePageParams(params)
+ })
+}
+
+export function fetchDeviceSiteList() {
+ return request.post({
+ url: '/deviceSite/list',
+ data: {}
+ })
+}
+
+export function fetchGetDeviceSiteDetail(id) {
+ return request.get({
+ url: `/deviceSite/${id}`
+ })
+}
+
+export function fetchGetDeviceSiteMany(ids) {
+ return request.post({
+ url: `/deviceSite/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveDeviceSite(params = {}) {
+ return request.post({
+ url: '/deviceSite/save',
+ params: buildDeviceSiteSavePayload(params)
+ })
+}
+
+export function fetchUpdateDeviceSite(params = {}) {
+ return request.post({
+ url: '/deviceSite/update',
+ params: buildDeviceSiteSavePayload(params)
+ })
+}
+
+export function fetchDeleteDeviceSite(ids) {
+ return request.post({
+ url: `/deviceSite/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchDeviceSiteQuery(condition = '') {
+ return request.post({
+ url: '/deviceSite/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export function fetchInitDeviceSite(params = {}) {
+ return request.post({
+ url: '/deviceSite/init',
+ params: buildDeviceSiteInitPayload(params)
+ })
+}
+
+export async function fetchExportDeviceSiteReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/deviceSite/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/flow-instance.js b/rsf-design/src/api/flow-instance.js
new file mode 100644
index 0000000..438040b
--- /dev/null
+++ b/rsf-design/src/api/flow-instance.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildFlowInstancePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchFlowInstancePage(params = {}) {
+ return request.post({
+ url: '/flowInstance/page',
+ params: buildFlowInstancePageParams(params)
+ })
+}
+
+export function fetchGetFlowInstanceDetail(id) {
+ return request.get({
+ url: `/flowInstance/${id}`
+ })
+}
+
+export function fetchGetFlowInstanceMany(ids) {
+ return request.post({
+ url: `/flowInstance/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportFlowInstanceReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/flowInstance/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/flow-step-instance.js b/rsf-design/src/api/flow-step-instance.js
new file mode 100644
index 0000000..bbb0888
--- /dev/null
+++ b/rsf-design/src/api/flow-step-instance.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildFlowStepInstancePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchFlowStepInstancePage(params = {}) {
+ return request.post({
+ url: '/flowStepInstance/page',
+ params: buildFlowStepInstancePageParams(params)
+ })
+}
+
+export function fetchGetFlowStepInstanceDetail(id) {
+ return request.get({
+ url: `/flowStepInstance/${id}`
+ })
+}
+
+export function fetchGetFlowStepInstanceMany(ids) {
+ return request.post({
+ url: `/flowStepInstance/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportFlowStepInstanceReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/flowStepInstance/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/flow-step-log.js b/rsf-design/src/api/flow-step-log.js
new file mode 100644
index 0000000..050b9c4
--- /dev/null
+++ b/rsf-design/src/api/flow-step-log.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildFlowStepLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchFlowStepLogPage(params = {}) {
+ return request.post({
+ url: '/flowStepLog/page',
+ params: buildFlowStepLogPageParams(params)
+ })
+}
+
+export function fetchGetFlowStepLogDetail(id) {
+ return request.get({
+ url: `/flowStepLog/${id}`
+ })
+}
+
+export function fetchGetFlowStepLogMany(ids) {
+ return request.post({
+ url: `/flowStepLog/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportFlowStepLogReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/flowStepLog/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/flow-step-template.js b/rsf-design/src/api/flow-step-template.js
new file mode 100644
index 0000000..8021896
--- /dev/null
+++ b/rsf-design/src/api/flow-step-template.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildFlowStepTemplatePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchFlowStepTemplatePage(params = {}) {
+ return request.post({
+ url: '/flowStepTemplate/page',
+ params: buildFlowStepTemplatePageParams(params)
+ })
+}
+
+export function fetchGetFlowStepTemplateDetail(id) {
+ return request.get({
+ url: `/flowStepTemplate/${id}`
+ })
+}
+
+export function fetchGetFlowStepTemplateMany(ids) {
+ return request.post({
+ url: `/flowStepTemplate/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportFlowStepTemplateReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/flowStepTemplate/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/freeze.js b/rsf-design/src/api/freeze.js
new file mode 100644
index 0000000..b93f140
--- /dev/null
+++ b/rsf-design/src/api/freeze.js
@@ -0,0 +1,45 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+export function buildFreezePageParams(params = {}) {
+ const entries = Object.entries(params).filter(([key, value]) => {
+ if (['current', 'pageSize', 'size'].includes(key)) {
+ return false
+ }
+ if (value === undefined || value === null) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...Object.fromEntries(entries.map(([key, value]) => [key, normalizeText(value)]))
+ }
+}
+
+export function fetchFreezePage(params = {}) {
+ return request.post({
+ url: '/locItem/useO/page',
+ params: buildFreezePageParams(params)
+ })
+}
+
+export function fetchFreezeDetail(id) {
+ return request.get({
+ url: `/locItem/${id}`
+ })
+}
+
+export function fetchEnabledFields() {
+ return request.get({
+ url: '/fields/enable/list'
+ })
+}
diff --git a/rsf-design/src/api/in-statistic-item.js b/rsf-design/src/api/in-statistic-item.js
new file mode 100644
index 0000000..2c15b2b
--- /dev/null
+++ b/rsf-design/src/api/in-statistic-item.js
@@ -0,0 +1,49 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildInStatisticItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ taskType: normalizeNumber(params.taskType, 1),
+ taskStatus: normalizeNumber(params.taskStatus, 100),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'taskType', 'taskStatus'])
+ }
+}
+
+export function fetchInStatisticItemPage(params = {}) {
+ return request.post({
+ url: '/inStatisticItem/page',
+ params: buildInStatisticItemPageParams(params)
+ })
+}
+
+export function fetchGetInStatisticItemDetail(id) {
+ return request.get({
+ url: `/stockStatistic/${id}`
+ })
+}
diff --git a/rsf-design/src/api/in-statistic.js b/rsf-design/src/api/in-statistic.js
new file mode 100644
index 0000000..c9c0da8
--- /dev/null
+++ b/rsf-design/src/api/in-statistic.js
@@ -0,0 +1,43 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildInStatisticPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ taskType: normalizeNumber(params.taskType, 1),
+ taskStatus: normalizeNumber(params.taskStatus, 100),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'taskType', 'taskStatus'])
+ }
+}
+
+export function fetchInStatisticPage(params = {}) {
+ return request.post({
+ url: '/inStatistic/page',
+ params: buildInStatisticPageParams(params)
+ })
+}
diff --git a/rsf-design/src/api/loc-area-mat-rela.js b/rsf-design/src/api/loc-area-mat-rela.js
new file mode 100644
index 0000000..1cb9289
--- /dev/null
+++ b/rsf-design/src/api/loc-area-mat-rela.js
@@ -0,0 +1,228 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeMultiIds(value) {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => normalizeNumber(item))
+ .filter((item) => item !== void 0 && item !== null)
+ }
+ const normalized = normalizeNumber(value)
+ return normalized === void 0 || normalized === null ? [] : [normalized]
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildLocAreaMatRelaPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildLocAreaMatRelaSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ areaMatId:
+ params.areaMatId !== undefined && params.areaMatId !== null && params.areaMatId !== ''
+ ? normalizeNumber(params.areaMatId)
+ : void 0,
+ areaId:
+ params.areaId !== undefined && params.areaId !== null && params.areaId !== ''
+ ? normalizeNumber(params.areaId)
+ : void 0,
+ code: normalizeText(params.code),
+ matnrId:
+ params.matnrId !== undefined && params.matnrId !== null && params.matnrId !== ''
+ ? normalizeNumber(params.matnrId)
+ : void 0,
+ groupId:
+ params.groupId !== undefined && params.groupId !== null && params.groupId !== ''
+ ? normalizeNumber(params.groupId)
+ : void 0,
+ locTypeId:
+ params.locTypeId !== undefined && params.locTypeId !== null && params.locTypeId !== ''
+ ? normalizeNumber(params.locTypeId)
+ : void 0,
+ locId:
+ params.locId !== undefined && params.locId !== null && params.locId !== ''
+ ? normalizeNumber(params.locId)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0,
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocAreaMatRelaSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: normalizeNumber(formData.id) }
+ : {}),
+ ...(formData.areaMatId !== undefined && formData.areaMatId !== null && formData.areaMatId !== ''
+ ? { areaMatId: normalizeNumber(formData.areaMatId) }
+ : {}),
+ ...(formData.areaId !== undefined && formData.areaId !== null && formData.areaId !== ''
+ ? { areaId: normalizeNumber(formData.areaId) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ ...(formData.matnrId !== undefined && formData.matnrId !== null && formData.matnrId !== ''
+ ? { matnrId: normalizeNumber(formData.matnrId) }
+ : {}),
+ ...(formData.groupId !== undefined && formData.groupId !== null && formData.groupId !== ''
+ ? { groupId: normalizeNumber(formData.groupId) }
+ : {}),
+ ...(formData.locTypeId !== undefined && formData.locTypeId !== null && formData.locTypeId !== ''
+ ? { locTypeId: normalizeNumber(formData.locTypeId) }
+ : {}),
+ ...(formData.locId !== undefined && formData.locId !== null && formData.locId !== ''
+ ? { locId: normalizeNumber(formData.locId) }
+ : {}),
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? normalizeNumber(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchLocAreaMatRelaPage(params = {}) {
+ return request.post({
+ url: '/locAreaMatRela/page',
+ params: buildLocAreaMatRelaPageParams(params)
+ })
+}
+
+export function fetchLocAreaMatRelaList() {
+ return request.post({
+ url: '/locAreaMatRela/list',
+ data: {}
+ })
+}
+
+export function fetchGetLocAreaMatRelaMany(ids) {
+ return request.post({
+ url: `/locAreaMatRela/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchGetLocAreaMatRelaDetail(id) {
+ return request.get({
+ url: `/locAreaMatRela/${id}`
+ })
+}
+
+export function fetchSaveLocAreaMatRela(params = {}) {
+ return request.post({
+ url: '/locAreaMatRela/save',
+ params: buildLocAreaMatRelaSavePayload(params)
+ })
+}
+
+export function fetchUpdateLocAreaMatRela(params = {}) {
+ return request.post({
+ url: '/locAreaMatRela/update',
+ params: buildLocAreaMatRelaSavePayload(params)
+ })
+}
+
+export function fetchDeleteLocAreaMatRela(ids) {
+ return request.post({
+ url: `/locAreaMatRela/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchLocAreaMatRelaQuery(condition = '') {
+ return request.post({
+ url: '/locAreaMatRela/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export function fetchGetLocAreaMatRelaGroups(id) {
+ return request.get({
+ url: `/locAreaMatRela/groups/${id}`
+ })
+}
+
+export function fetchGetLocAreaMatRelaLocType(id) {
+ return request.get({
+ url: `/locAreaMatRela/locType/${id}`
+ })
+}
+
+export function fetchBindLocAreaMatRelaByMatnr(payload = {}) {
+ return request.post({
+ url: '/locAreaMatRela/matnr/bind',
+ params: {
+ ...(payload.areaMatId !== undefined && payload.areaMatId !== null && payload.areaMatId !== ''
+ ? { areaMatId: normalizeNumber(payload.areaMatId) }
+ : {}),
+ ...(payload.warehouseId !== undefined && payload.warehouseId !== null && payload.warehouseId !== ''
+ ? { warehouseId: normalizeNumber(payload.warehouseId) }
+ : {}),
+ ...(payload.areaId !== undefined && payload.areaId !== null && payload.areaId !== ''
+ ? { areaId: normalizeNumber(payload.areaId) }
+ : {}),
+ ...(payload.groupId !== undefined && payload.groupId !== null && payload.groupId !== ''
+ ? { groupId: normalizeNumber(payload.groupId) }
+ : {}),
+ matnrId: normalizeMultiIds(payload.matnrId),
+ typeId: normalizeMultiIds(payload.typeId),
+ locId: normalizeMultiIds(payload.locId)
+ }
+ })
+}
+
+export async function fetchExportLocAreaMatRelaReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/locAreaMatRela/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/loc-area-mat.js b/rsf-design/src/api/loc-area-mat.js
new file mode 100644
index 0000000..814be69
--- /dev/null
+++ b/rsf-design/src/api/loc-area-mat.js
@@ -0,0 +1,240 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeMultiIds(value) {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => normalizeNumber(item))
+ .filter((item) => item !== void 0 && item !== null)
+ }
+ const normalized = normalizeNumber(value)
+ return normalized === void 0 || normalized === null ? [] : [normalized]
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildLocAreaMatPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildLocAreaMatSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ warehouseId:
+ params.warehouseId !== undefined && params.warehouseId !== null && params.warehouseId !== ''
+ ? normalizeNumber(params.warehouseId)
+ : void 0,
+ areaId:
+ params.areaId !== undefined && params.areaId !== null && params.areaId !== ''
+ ? normalizeNumber(params.areaId)
+ : void 0,
+ depict: normalizeText(params.depict),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocAreaMatSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: normalizeNumber(formData.id) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ warehouseId:
+ formData.warehouseId !== undefined && formData.warehouseId !== null && formData.warehouseId !== ''
+ ? normalizeNumber(formData.warehouseId)
+ : void 0,
+ areaId:
+ formData.areaId !== undefined && formData.areaId !== null && formData.areaId !== ''
+ ? normalizeNumber(formData.areaId)
+ : void 0,
+ depict: normalizeText(formData.depict) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? normalizeNumber(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildLocAreaMatRelationBindPayload(formData = {}) {
+ return {
+ areaMatId:
+ formData.areaMatId !== undefined && formData.areaMatId !== null && formData.areaMatId !== ''
+ ? normalizeNumber(formData.areaMatId)
+ : void 0,
+ warehouseId:
+ formData.warehouseId !== undefined && formData.warehouseId !== null && formData.warehouseId !== ''
+ ? normalizeNumber(formData.warehouseId)
+ : void 0,
+ areaId:
+ formData.areaId !== undefined && formData.areaId !== null && formData.areaId !== ''
+ ? normalizeNumber(formData.areaId)
+ : void 0,
+ groupId:
+ formData.groupId !== undefined && formData.groupId !== null && formData.groupId !== ''
+ ? normalizeNumber(formData.groupId)
+ : void 0,
+ matnrId: normalizeMultiIds(formData.matnrId),
+ typeId: normalizeMultiIds(formData.typeId),
+ locId: normalizeMultiIds(formData.locId)
+ }
+}
+
+export function fetchLocAreaMatPage(params = {}) {
+ return request.post({
+ url: '/locAreaMat/page',
+ params: buildLocAreaMatPageParams(params)
+ })
+}
+
+export function fetchLocAreaMatList() {
+ return request.post({
+ url: '/locAreaMat/list',
+ data: {}
+ })
+}
+
+export function fetchGetLocAreaMatMany(ids) {
+ return request.post({
+ url: `/locAreaMat/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchGetLocAreaMatDetail(id) {
+ return request.get({
+ url: `/locAreaMat/${id}`
+ })
+}
+
+export function fetchSaveLocAreaMat(params = {}) {
+ return request.post({
+ url: '/locAreaMat/save',
+ params: buildLocAreaMatSavePayload(params)
+ })
+}
+
+export function fetchUpdateLocAreaMat(params = {}) {
+ return request.post({
+ url: '/locAreaMat/update',
+ params: buildLocAreaMatSavePayload(params)
+ })
+}
+
+export function fetchDeleteLocAreaMat(ids) {
+ return request.post({
+ url: `/locAreaMat/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchLocAreaMatQuery(condition = '') {
+ return request.post({
+ url: '/locAreaMat/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportLocAreaMatReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/locAreaMat/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
+
+export function fetchLocAreaMatRelaPage(params = {}) {
+ return request.post({
+ url: '/locAreaMatRela/page',
+ params: buildLocAreaMatPageParams(params)
+ })
+}
+
+export function fetchGetLocAreaMatRelaMany(ids) {
+ return request.post({
+ url: `/locAreaMatRela/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchGetLocAreaMatRelaDetail(id) {
+ return request.get({
+ url: `/locAreaMatRela/${id}`
+ })
+}
+
+export function fetchDeleteLocAreaMatRela(ids) {
+ return request.post({
+ url: `/locAreaMatRela/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchGetLocAreaMatRelaGroups(id) {
+ return request.get({
+ url: `/locAreaMatRela/groups/${id}`
+ })
+}
+
+export function fetchGetLocAreaMatRelaLocType(id) {
+ return request.get({
+ url: `/locAreaMatRela/locType/${id}`
+ })
+}
+
+export function fetchBindLocAreaMatByMatnr(payload = {}) {
+ return request.post({
+ url: '/locAreaMatRela/matnr/bind',
+ params: buildLocAreaMatRelationBindPayload(payload)
+ })
+}
+
diff --git a/rsf-design/src/api/loc-area-rela.js b/rsf-design/src/api/loc-area-rela.js
new file mode 100644
index 0000000..f804761
--- /dev/null
+++ b/rsf-design/src/api/loc-area-rela.js
@@ -0,0 +1,157 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildLocAreaRelaPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildLocAreaRelaSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ locAreaId:
+ params.locAreaId !== undefined && params.locAreaId !== null && params.locAreaId !== ''
+ ? normalizeNumber(params.locAreaId)
+ : void 0,
+ locId:
+ params.locId !== undefined && params.locId !== null && params.locId !== ''
+ ? normalizeNumber(params.locId)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(
+ ([, value]) => value !== '' && value !== void 0 && value !== null
+ )
+ )
+}
+
+export function buildLocAreaRelaSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: normalizeNumber(formData.id) }
+ : {}),
+ ...(formData.locAreaId !== undefined && formData.locAreaId !== null && formData.locAreaId !== ''
+ ? { locAreaId: normalizeNumber(formData.locAreaId) }
+ : {}),
+ ...(formData.locId !== undefined && formData.locId !== null && formData.locId !== ''
+ ? { locId: normalizeNumber(formData.locId) }
+ : {}),
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? normalizeNumber(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchLocAreaRelaPage(params = {}) {
+ return request.post({
+ url: '/locAreaRela/page',
+ params: buildLocAreaRelaPageParams(params)
+ })
+}
+
+export function fetchLocAreaRelaList() {
+ return request.post({
+ url: '/locAreaRela/list',
+ data: {}
+ })
+}
+
+export function fetchLocAreaRelaMany(ids) {
+ return request.post({
+ url: `/locAreaRela/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchGetLocAreaRelaDetail(id) {
+ return request.get({
+ url: `/locAreaRela/${id}`
+ })
+}
+
+export function fetchSaveLocAreaRela(params = {}) {
+ return request.post({
+ url: '/locAreaRela/save',
+ params: buildLocAreaRelaSavePayload(params)
+ })
+}
+
+export function fetchUpdateLocAreaRela(params = {}) {
+ return request.post({
+ url: '/locAreaRela/update',
+ params: buildLocAreaRelaSavePayload(params)
+ })
+}
+
+export function fetchDeleteLocAreaRela(ids) {
+ return request.post({
+ url: `/locAreaRela/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchLocAreaRelaQuery(condition = '') {
+ return request.post({
+ url: '/locAreaRela/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportLocAreaRelaReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/locAreaRela/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/loc-area.js b/rsf-design/src/api/loc-area.js
new file mode 100644
index 0000000..bf345ae
--- /dev/null
+++ b/rsf-design/src/api/loc-area.js
@@ -0,0 +1,142 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildLocAreaPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildLocAreaSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ areaId:
+ params.areaId !== undefined && params.areaId !== null && params.areaId !== ''
+ ? Number(params.areaId)
+ : void 0,
+ name: normalizeText(params.name),
+ code: normalizeText(params.code),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocAreaSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.areaId !== undefined && formData.areaId !== null && formData.areaId !== ''
+ ? { areaId: Number(formData.areaId) }
+ : {}),
+ name: normalizeText(formData.name) || '',
+ code: normalizeText(formData.code) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchLocAreaPage(params = {}) {
+ return request.post({
+ url: '/locArea/page',
+ params: buildLocAreaPageParams(params)
+ })
+}
+
+export function fetchLocAreaList() {
+ return request.post({
+ url: '/locArea/list',
+ data: {}
+ })
+}
+
+export function fetchLocAreaMany(ids) {
+ return request.post({
+ url: `/locArea/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchGetLocAreaDetail(id) {
+ return request.get({
+ url: `/locArea/${id}`
+ })
+}
+
+export function fetchSaveLocArea(params = {}) {
+ return request.post({
+ url: '/locArea/save',
+ params: buildLocAreaSavePayload(params)
+ })
+}
+
+export function fetchUpdateLocArea(params = {}) {
+ return request.post({
+ url: '/locArea/update',
+ params: buildLocAreaSavePayload(params)
+ })
+}
+
+export function fetchDeleteLocArea(ids) {
+ return request.post({
+ url: `/locArea/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchLocAreaQuery(condition = '') {
+ return request.post({
+ url: '/locArea/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportLocAreaReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/locArea/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/loc-dead-report.js b/rsf-design/src/api/loc-dead-report.js
new file mode 100644
index 0000000..bff09c0
--- /dev/null
+++ b/rsf-design/src/api/loc-dead-report.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildLocDeadReportPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchLocDeadReportPage(params = {}) {
+ return request.post({
+ url: '/locDeadReport/page',
+ params: buildLocDeadReportPageParams(params)
+ })
+}
+
+export function fetchGetLocDeadReportDetail(id) {
+ return request.get({
+ url: `/locDeadReport/${id}`
+ })
+}
+
+export function fetchGetLocDeadReportMany(ids) {
+ return request.post({
+ url: `/locDeadReport/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportLocDeadReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/locDeadReport/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/loc-item.js b/rsf-design/src/api/loc-item.js
new file mode 100644
index 0000000..f981f08
--- /dev/null
+++ b/rsf-design/src/api/loc-item.js
@@ -0,0 +1,81 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function withTextParam(target, key, value) {
+ const normalized = normalizeText(value)
+ if (normalized) {
+ target[key] = normalized
+ }
+}
+
+function withNumberParam(target, key, value) {
+ const normalized = normalizeNumber(value)
+ if (normalized !== null) {
+ target[key] = normalized
+ }
+}
+
+export function buildLocItemPageParams(params = {}) {
+ const payload = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+
+ ;[
+ 'condition',
+ 'type',
+ 'maktx',
+ 'matnrCode',
+ 'trackCode',
+ 'unit',
+ 'batch',
+ 'splrBatch',
+ 'spec',
+ 'model',
+ 'fieldsIndex',
+ 'memo'
+ ].forEach((key) => withTextParam(payload, key, params[key]))
+
+ ;['locId', 'orderId', 'orderItemId', 'wkType', 'matnrId', 'anfme', 'status'].forEach((key) =>
+ withNumberParam(payload, key, params[key])
+ )
+
+ if (params.timeStart) {
+ payload.timeStart = params.timeStart
+ }
+ if (params.timeEnd) {
+ payload.timeEnd = params.timeEnd
+ }
+
+ return payload
+}
+
+export function fetchLocItemPage(params = {}) {
+ return request.post({
+ url: '/locItem/page',
+ params: buildLocItemPageParams(params)
+ })
+}
+
+export function fetchLocItemDetail(id) {
+ return request.get({
+ url: `/locItem/${id}`
+ })
+}
+
+export function fetchEnabledFields() {
+ return request.get({
+ url: '/fields/enable/list'
+ })
+}
diff --git a/rsf-design/src/api/loc-preview.js b/rsf-design/src/api/loc-preview.js
new file mode 100644
index 0000000..e016e31
--- /dev/null
+++ b/rsf-design/src/api/loc-preview.js
@@ -0,0 +1,51 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+export function buildLocPreviewPageParams(params = {}) {
+ const condition = normalizeText(params.condition)
+ const code = normalizeText(params.code)
+ const barcode = normalizeText(params.barcode)
+
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(condition ? { condition } : {}),
+ ...(code ? { code } : {}),
+ ...(barcode ? { barcode } : {})
+ }
+}
+
+export function buildLocPreviewItemsPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.locId !== undefined ? { locId: params.locId } : {})
+ }
+}
+
+export function fetchLocPreviewPage(params = {}) {
+ return request.post({
+ url: '/locPreview/page',
+ params: buildLocPreviewPageParams(params)
+ })
+}
+
+export function fetchLocPreviewDetail(id) {
+ return request.get({
+ url: `/locPreview/${id}`
+ })
+}
+
+export function fetchLocPreviewItemsPage(params = {}) {
+ return request.post({
+ url: '/locItem/page',
+ params: buildLocPreviewItemsPageParams(params)
+ })
+}
+
+export function fetchEnabledFields() {
+ return request.get({ url: '/fields/enable/list' })
+}
diff --git a/rsf-design/src/api/loc-revise.js b/rsf-design/src/api/loc-revise.js
new file mode 100644
index 0000000..a04360b
--- /dev/null
+++ b/rsf-design/src/api/loc-revise.js
@@ -0,0 +1,139 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) {
+ return false
+ }
+ if (value === undefined || value === null) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildLocRevisePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildReviseLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildReviseLogItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchLocRevisePage(params = {}) {
+ return request.post({
+ url: '/locRevise/page',
+ params: buildLocRevisePageParams(params)
+ })
+}
+
+export function fetchGetLocReviseDetail(id) {
+ return request.get({
+ url: `/locRevise/${id}`
+ })
+}
+
+export function fetchSaveLocRevise(payload = {}) {
+ return request.post({
+ url: '/locRevise/save',
+ data: payload,
+ showSuccessMessage: true
+ })
+}
+
+export function fetchUpdateLocRevise(payload = {}) {
+ return request.post({
+ url: '/locRevise/update',
+ data: payload,
+ showSuccessMessage: true
+ })
+}
+
+export function fetchDeleteLocRevise(ids) {
+ return request.post({
+ url: `/locRevise/remove/${normalizeIds(ids)}`,
+ showSuccessMessage: true
+ })
+}
+
+export function fetchGetLocReviseMany(ids) {
+ return request.post({
+ url: `/locRevise/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchReviseLogPage(params = {}) {
+ return request.post({
+ url: '/reviseLog/page',
+ params: buildReviseLogPageParams(params)
+ })
+}
+
+export function fetchReviseLogItemPage(params = {}) {
+ return request.post({
+ url: '/reviseLogItem/page',
+ params: buildReviseLogItemPageParams(params)
+ })
+}
+
+export function fetchCompleteLocRevise(id) {
+ return request.post({
+ url: `/reviseLog/complete/${id}`,
+ showSuccessMessage: true
+ })
+}
+
+export function fetchWarehouseAreasList() {
+ return request.post({
+ url: '/warehouseAreas/list',
+ data: {}
+ })
+}
+
+export async function fetchExportLocReviseReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/locRevise/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/loc-type.js b/rsf-design/src/api/loc-type.js
new file mode 100644
index 0000000..3726026
--- /dev/null
+++ b/rsf-design/src/api/loc-type.js
@@ -0,0 +1,139 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildLocTypePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildLocTypeSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ uuid: normalizeText(params.uuid),
+ code: normalizeText(params.code),
+ name: normalizeText(params.name),
+ regex: normalizeText(params.regex),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocTypeSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ name: normalizeText(formData.name) || '',
+ regex: normalizeText(formData.regex) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchLocTypePage(params = {}) {
+ return request.post({
+ url: '/locType/page',
+ params: buildLocTypePageParams(params)
+ })
+}
+
+export function fetchLocTypeList() {
+ return request.post({
+ url: '/locType/list',
+ data: {}
+ })
+}
+
+export function fetchLocTypeMany(ids) {
+ return request.post({
+ url: `/locType/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchGetLocTypeDetail(id) {
+ return request.get({
+ url: `/locType/${id}`
+ })
+}
+
+export function fetchSaveLocType(params = {}) {
+ return request.post({
+ url: '/locType/save',
+ params: buildLocTypeSavePayload(params)
+ })
+}
+
+export function fetchUpdateLocType(params = {}) {
+ return request.post({
+ url: '/locType/update',
+ params: buildLocTypeSavePayload(params)
+ })
+}
+
+export function fetchDeleteLocType(ids) {
+ return request.post({
+ url: `/locType/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchLocTypeQuery(condition = '') {
+ return request.post({
+ url: '/locType/query',
+ data: {},
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportLocTypeReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/locType/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/matnr-group.js b/rsf-design/src/api/matnr-group.js
new file mode 100644
index 0000000..f65af69
--- /dev/null
+++ b/rsf-design/src/api/matnr-group.js
@@ -0,0 +1,154 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return void 0
+ }
+ const normalized = Number(value)
+ return Number.isNaN(normalized) ? void 0 : normalized
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
+ )
+}
+
+export function buildMatnrGroupPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildMatnrGroupSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ name: normalizeText(params.name),
+ parCode: normalizeText(params.parCode),
+ memo: normalizeText(params.memo),
+ sort: normalizeNumber(params.sort),
+ status: normalizeNumber(params.status),
+ parentId: normalizeNumber(params.parentId)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildMatnrGroupSavePayload(params = {}) {
+ return {
+ ...(params.id !== undefined && params.id !== null && params.id !== ''
+ ? { id: Number(params.id) }
+ : {}),
+ parentId:
+ params.parentId !== undefined && params.parentId !== null && params.parentId !== ''
+ ? Number(params.parentId)
+ : 0,
+ parCode: normalizeText(params.parCode) || '',
+ code: normalizeText(params.code) || '',
+ name: normalizeText(params.name) || '',
+ sort:
+ params.sort !== undefined && params.sort !== null && params.sort !== ''
+ ? Number(params.sort)
+ : 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : 1,
+ memo: normalizeText(params.memo) || ''
+ }
+}
+
+export function fetchMatnrGroupPage(params = {}) {
+ return request.post({
+ url: '/matnrGroup/page',
+ params: buildMatnrGroupPageParams(params)
+ })
+}
+
+export function fetchGetMatnrGroupDetail(id) {
+ return request.get({
+ url: `/matnrGroup/${id}`
+ })
+}
+
+export function fetchGetMatnrGroupMany(ids) {
+ return request.post({
+ url: `/matnrGroup/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveMatnrGroup(params = {}) {
+ return request.post({
+ url: '/matnrGroup/save',
+ params: buildMatnrGroupSavePayload(params)
+ })
+}
+
+export function fetchUpdateMatnrGroup(params = {}) {
+ return request.post({
+ url: '/matnrGroup/update',
+ params: buildMatnrGroupSavePayload(params)
+ })
+}
+
+export function fetchDeleteMatnrGroup(ids) {
+ return request.post({
+ url: `/matnrGroup/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchMatnrGroupQuery(condition = '') {
+ return request.post({
+ url: '/matnrGroup/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export function fetchMatnrGroupTree(params = {}) {
+ return request.post({
+ url: '/matnrGroup/tree',
+ params: {
+ condition: normalizeText(params.condition)
+ }
+ })
+}
+
+export async function fetchExportMatnrGroupReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/matnrGroup/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/out-bound.js b/rsf-design/src/api/out-bound.js
new file mode 100644
index 0000000..0eded96
--- /dev/null
+++ b/rsf-design/src/api/out-bound.js
@@ -0,0 +1,103 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeOptionalText(value) {
+ const text = normalizeText(value)
+ return text === '-' ? '' : text
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return value
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : value
+}
+
+function compactDefinedFields(record = {}) {
+ return Object.fromEntries(
+ Object.entries(record).filter(([, value]) => value !== undefined && value !== null && value !== '')
+ )
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, typeof value === 'string' ? normalizeText(value) : value])
+ )
+}
+
+export function buildOutBoundInventoryPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchOutboundInventoryPage(params = {}) {
+ return request.post({
+ url: '/locItem/useO/page',
+ params: buildOutBoundInventoryPageParams(params)
+ })
+}
+
+export function fetchGetOutboundInventoryDetail(id) {
+ return request.get({
+ url: `/locItem/${id}`
+ })
+}
+
+export function fetchGenerateOutboundTask(payload = {}) {
+ return request.post({
+ url: '/locItem/generate/task',
+ data: payload
+ })
+}
+
+export function fetchOutboundSiteList(params = {}) {
+ return request.post({
+ url: '/selectStaList/list',
+ params: {
+ type: params.type || [101, 103]
+ }
+ })
+}
+
+export function normalizeOutboundTaskPayload(payload = {}) {
+ return {
+ ...payload,
+ siteNo: normalizeOptionalText(payload.siteNo),
+ memo: normalizeText(payload.memo),
+ items: Array.isArray(payload.items)
+ ? payload.items.map((item) =>
+ compactDefinedFields({
+ ...item,
+ id: normalizeNumber(item.id),
+ locId: normalizeNumber(item.locId),
+ orderId: normalizeNumber(item.orderId),
+ orderItemId: normalizeNumber(item.orderItemId),
+ wkType: normalizeNumber(item.wkType),
+ matnrId: normalizeNumber(item.matnrId),
+ outQty: Number(normalizeNumber(item.outQty) || 0),
+ anfme: Number(normalizeNumber(item.anfme) || 0),
+ workQty: Number(normalizeNumber(item.workQty) || 0),
+ qty: Number(normalizeNumber(item.qty) || 0),
+ channel: normalizeNumber(item.channel),
+ status: normalizeNumber(item.status),
+ siteNo: normalizeOptionalText(item.siteNo),
+ memo: normalizeOptionalText(item.memo)
+ })
+ )
+ : []
+ }
+}
diff --git a/rsf-design/src/api/out-statistic-item.js b/rsf-design/src/api/out-statistic-item.js
new file mode 100644
index 0000000..e6bd055
--- /dev/null
+++ b/rsf-design/src/api/out-statistic-item.js
@@ -0,0 +1,43 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildOutStatisticItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ taskType: normalizeNumber(params.taskType, 101),
+ taskStatus: normalizeNumber(params.taskStatus, 200),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'taskType', 'taskStatus'])
+ }
+}
+
+export function fetchOutStatisticItemPage(params = {}) {
+ return request.post({
+ url: '/outStatisticItem/page',
+ params: buildOutStatisticItemPageParams(params)
+ })
+}
diff --git a/rsf-design/src/api/out-statistic.js b/rsf-design/src/api/out-statistic.js
new file mode 100644
index 0000000..f644967
--- /dev/null
+++ b/rsf-design/src/api/out-statistic.js
@@ -0,0 +1,43 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildOutStatisticPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ taskType: normalizeNumber(params.taskType, 101),
+ taskStatus: normalizeNumber(params.taskStatus, 200),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'taskType', 'taskStatus'])
+ }
+}
+
+export function fetchOutStatisticPage(params = {}) {
+ return request.post({
+ url: '/outStatistic/page',
+ params: buildOutStatisticPageParams(params)
+ })
+}
diff --git a/rsf-design/src/api/out-stock-item.js b/rsf-design/src/api/out-stock-item.js
new file mode 100644
index 0000000..591471d
--- /dev/null
+++ b/rsf-design/src/api/out-stock-item.js
@@ -0,0 +1,97 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+
+ return String(ids).trim()
+}
+
+export function buildOutStockItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'orderCode',
+ 'poCode',
+ 'platItemId',
+ 'matnrCode',
+ 'maktx',
+ 'batch',
+ 'splrBatch',
+ 'trackCode',
+ 'barcode',
+ 'fieldsIndex'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ const numericStatus = Number(params.status)
+ if (!Number.isNaN(numericStatus)) {
+ result.status = numericStatus
+ }
+ }
+
+ return result
+}
+
+export function buildOutStockItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildOutStockItemSearchParams(params)
+ }
+}
+
+export function fetchOutStockItemPage(params = {}) {
+ return request.post({
+ url: '/outStockItem/page',
+ params: buildOutStockItemPageParams(params)
+ })
+}
+
+export function fetchOutStockItemList(params = {}) {
+ return request.post({
+ url: '/outStockItem/list',
+ params: buildOutStockItemSearchParams(params)
+ })
+}
+
+export function fetchGetOutStockItemDetail(id) {
+ return request.get({
+ url: `/outStockItem/${id}`
+ })
+}
+
+export function fetchGetOutStockItemMany(ids) {
+ return request.post({
+ url: `/outStockItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportOutStockItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/outStockItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/out-stock.js b/rsf-design/src/api/out-stock.js
new file mode 100644
index 0000000..9e2d9e8
--- /dev/null
+++ b/rsf-design/src/api/out-stock.js
@@ -0,0 +1,75 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((item) => String(item).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildOutStockPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ type: 'out',
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchOutStockPage(params = {}) {
+ return request.post({ url: '/outStock/page', params: buildOutStockPageParams(params) })
+}
+
+export function fetchGetOutStockDetail(id) {
+ return request.get({ url: `/outStock/${id}` })
+}
+
+export function fetchGetOutStockMany(ids) {
+ return request.post({ url: `/outStock/many/${normalizeIds(ids)}` })
+}
+
+export function fetchDeleteOutStock(ids) {
+ return request.post({ url: `/outStock/remove/${normalizeIds(ids)}` })
+}
+
+export function fetchCompleteOutStock(id) {
+ return request.get({ url: `/outStock/complete/${id}` })
+}
+
+export function fetchCancelOutStock(id) {
+ return request.get({ url: `/outStock/cancel/${id}` })
+}
+
+export async function fetchExportOutStockReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/outStock/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/preparation-item.js b/rsf-design/src/api/preparation-item.js
new file mode 100644
index 0000000..4a0b7e2
--- /dev/null
+++ b/rsf-design/src/api/preparation-item.js
@@ -0,0 +1,78 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) {
+ return false
+ }
+ if (value === null || value === undefined) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildPreparationItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.orderId !== '' && params.orderId !== undefined && params.orderId !== null
+ ? { orderId: Number(params.orderId) }
+ : {}),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'orderId'])
+ }
+}
+
+export function fetchPreparationItemPage(params = {}) {
+ return request.post({
+ url: '/outStockItem/page',
+ params: buildPreparationItemPageParams(params)
+ })
+}
+
+export function fetchGetPreparationItemDetail(id) {
+ return request.get({
+ url: `/outStockItem/${id}`
+ })
+}
+
+export function fetchGetPreparationItemMany(ids) {
+ return request.post({
+ url: `/outStockItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportPreparationItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/outStockItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/preparation.js b/rsf-design/src/api/preparation.js
new file mode 100644
index 0000000..0049199
--- /dev/null
+++ b/rsf-design/src/api/preparation.js
@@ -0,0 +1,90 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((item) => String(item).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildPreparationPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildPreparationItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.orderId !== undefined ? { orderId: Number(params.orderId) } : {}),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'orderId'])
+ }
+}
+
+export function fetchPreparationPage(params = {}) {
+ return request.post({ url: '/preparation/page', params: buildPreparationPageParams(params) })
+}
+
+export function fetchPreparationItemPage(params = {}) {
+ return request.post({
+ url: '/outStockItem/page',
+ params: buildPreparationItemPageParams(params)
+ })
+}
+
+export function fetchGetPreparationDetail(id) {
+ return request.get({ url: `/preparation/${id}` })
+}
+
+export function fetchGetPreparationMany(ids) {
+ return request.post({ url: `/preparation/many/${normalizeIds(ids)}` })
+}
+
+export function fetchDeletePreparation(ids) {
+ return request.post({ url: `/preparation/remove/${normalizeIds(ids)}` })
+}
+
+export function fetchCompletePreparation(id) {
+ return request.get({ url: `/preparation/complete/${id}` })
+}
+
+export function fetchCancelPreparation(id) {
+ return request.get({ url: `/preparation/cancel/${id}` })
+}
+
+export async function fetchExportPreparationReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/preparation/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/purchase-item.js b/rsf-design/src/api/purchase-item.js
new file mode 100644
index 0000000..0b41c13
--- /dev/null
+++ b/rsf-design/src/api/purchase-item.js
@@ -0,0 +1,95 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) {
+ return false
+ }
+ if (value === null || value === undefined) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildPurchaseItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildPurchaseItemSearchParams(params = {}) {
+ return filterParams(params)
+}
+
+export function fetchPurchaseItemPage(params = {}) {
+ return request.post({
+ url: '/purchaseItem/page',
+ params: buildPurchaseItemPageParams(params)
+ })
+}
+
+export function fetchPurchaseItemList(params = {}) {
+ return request.post({
+ url: '/purchaseItem/list',
+ data: buildPurchaseItemSearchParams(params)
+ })
+}
+
+export function fetchPurchaseItemMany(ids) {
+ return request.post({
+ url: `/purchaseItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchPurchaseItemDetail(id) {
+ return request.get({
+ url: `/purchaseItem/${id}`
+ })
+}
+
+export function fetchPurchaseItemQuery(condition = '') {
+ return request.post({
+ url: '/purchaseItem/query',
+ params: {
+ condition: normalizeText(condition)
+ }
+ })
+}
+
+export async function fetchExportPurchaseItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/purchaseItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/purchase.js b/rsf-design/src/api/purchase.js
new file mode 100644
index 0000000..bfa56bc
--- /dev/null
+++ b/rsf-design/src/api/purchase.js
@@ -0,0 +1,118 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildPurchasePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildPurchaseItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchPurchasePage(params = {}) {
+ return request.post({
+ url: '/purchase/page',
+ params: buildPurchasePageParams(params)
+ })
+}
+
+export function fetchPurchaseList() {
+ return request.post({
+ url: '/purchase/list',
+ data: {}
+ })
+}
+
+export function fetchPurchaseMany(ids) {
+ return request.post({
+ url: `/purchase/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchPurchaseDetail(id) {
+ return request.get({
+ url: `/purchase/${id}`
+ })
+}
+
+export function fetchSavePurchase(params = {}) {
+ return request.post({
+ url: '/purchase/save',
+ params
+ })
+}
+
+export function fetchUpdatePurchase(params = {}) {
+ return request.post({
+ url: '/purchase/update',
+ params
+ })
+}
+
+export function fetchDeletePurchase(ids) {
+ return request.post({
+ url: `/purchase/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchPurchaseQuery(condition = '') {
+ return request.post({
+ url: '/purchase/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export function fetchPurchaseItemPage(params = {}) {
+ return request.post({
+ url: '/purchaseItem/page',
+ params: buildPurchaseItemPageParams(params)
+ })
+}
+
+export async function fetchExportPurchaseReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/purchase/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/qly-inspect.js b/rsf-design/src/api/qly-inspect.js
new file mode 100644
index 0000000..1d37dde
--- /dev/null
+++ b/rsf-design/src/api/qly-inspect.js
@@ -0,0 +1,76 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+export function buildQlyInspectPageParams(params = {}) {
+ const entries = Object.entries(params).filter(([key, value]) => {
+ if (['current', 'pageSize', 'size'].includes(key)) {
+ return false
+ }
+ if (value === undefined || value === null) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...Object.fromEntries(entries.map(([key, value]) => [key, normalizeText(value)]))
+ }
+}
+
+export function buildQlyInspectItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.ispectId !== undefined ? { ispectId: params.ispectId } : {})
+ }
+}
+
+export function fetchQlyInspectPage(params = {}) {
+ return request.post({
+ url: '/qlyInspect/page',
+ params: buildQlyInspectPageParams(params)
+ })
+}
+
+export function fetchQlyInspectItemPage(params = {}) {
+ return request.post({
+ url: '/qlyIsptItem/page',
+ params: buildQlyInspectItemPageParams(params)
+ })
+}
+
+export function fetchGetQlyInspectMany(ids) {
+ const normalizedIds = normalizeIds(ids)
+ return request.post({
+ url: `/qlyInspect/many/${normalizedIds}`
+ })
+}
+
+export async function fetchExportQlyInspectReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/qlyInspect/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/qly-ispt-item.js b/rsf-design/src/api/qly-ispt-item.js
new file mode 100644
index 0000000..9364484
--- /dev/null
+++ b/rsf-design/src/api/qly-ispt-item.js
@@ -0,0 +1,110 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function normalizeNumericParam(value) {
+ if (value === '' || value === null || value === undefined) {
+ return undefined
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : value
+}
+
+export function buildQlyIsptItemPageParams(params = {}) {
+ const result = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+
+ ;[
+ 'condition',
+ 'matnrCode',
+ 'maktx',
+ 'label',
+ 'splrName',
+ 'splrBatch',
+ 'stockBatch',
+ 'platOrderCode',
+ 'platWorkCode',
+ 'projectCode',
+ 'picPath',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value !== undefined && value !== null && value !== '') {
+ result[key] = value
+ }
+ })
+
+ ;['ispectId', 'rcptQty', 'dlyQty', 'disQty', 'safeQty', 'status'].forEach((key) => {
+ const value = normalizeNumericParam(params[key])
+ if (value !== undefined && value !== '') {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function buildQlyIsptItemResultPageParams(params = {}) {
+ const result = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+
+ const inspectId = normalizeNumericParam(params.id ?? params.ispectId)
+ if (inspectId !== undefined && inspectId !== '') {
+ result.id = inspectId
+ }
+
+ return result
+}
+
+export function fetchQlyIsptItemPage(params = {}) {
+ return request.post({
+ url: '/qlyIsptItem/page',
+ params: buildQlyIsptItemPageParams(params)
+ })
+}
+
+export function fetchQlyIsptItemResultPage(params = {}) {
+ return request.post({
+ url: '/qlyIsptItem/ispt/result/page',
+ params: buildQlyIsptItemResultPageParams(params)
+ })
+}
+
+export function fetchGetQlyIsptItemDetail(id) {
+ return request.get({
+ url: `/qlyIsptItem/${id}`
+ })
+}
+
+export function fetchGetQlyIsptItemMany(ids) {
+ return request.post({
+ url: `/qlyIsptItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportQlyIsptItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/qlyIsptItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/revise-log-item.js b/rsf-design/src/api/revise-log-item.js
new file mode 100644
index 0000000..0a23b61
--- /dev/null
+++ b/rsf-design/src/api/revise-log-item.js
@@ -0,0 +1,61 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) {
+ return false
+ }
+ if (value === undefined || value === null) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildReviseLogItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchReviseLogItemPage(params = {}) {
+ return request.post({
+ url: '/reviseLogItem/page',
+ params: buildReviseLogItemPageParams(params)
+ })
+}
+
+export function fetchGetReviseLogItemDetail(id) {
+ return request.get({
+ url: `/reviseLogItem/${id}`
+ })
+}
+
+export function fetchGetReviseLogItemMany(ids) {
+ return request.post({
+ url: `/reviseLogItem/many/${normalizeIds(ids)}`
+ })
+}
diff --git a/rsf-design/src/api/revise-log.js b/rsf-design/src/api/revise-log.js
new file mode 100644
index 0000000..08a9c6b
--- /dev/null
+++ b/rsf-design/src/api/revise-log.js
@@ -0,0 +1,61 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) {
+ return false
+ }
+ if (value === undefined || value === null) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildReviseLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchReviseLogPage(params = {}) {
+ return request.post({
+ url: '/reviseLog/page',
+ params: buildReviseLogPageParams(params)
+ })
+}
+
+export function fetchGetReviseLogDetail(id) {
+ return request.get({
+ url: `/reviseLog/${id}`
+ })
+}
+
+export function fetchGetReviseLogMany(ids) {
+ return request.post({
+ url: `/reviseLog/many/${normalizeIds(ids)}`
+ })
+}
diff --git a/rsf-design/src/api/serial-rule-item.js b/rsf-design/src/api/serial-rule-item.js
new file mode 100644
index 0000000..e365095
--- /dev/null
+++ b/rsf-design/src/api/serial-rule-item.js
@@ -0,0 +1,157 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
+ )
+}
+
+export function buildSerialRuleItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildSerialRuleItemSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.ruleId !== void 0 && formData.ruleId !== null && formData.ruleId !== ''
+ ? { ruleId: Number(formData.ruleId) }
+ : {}),
+ wkType: normalizeText(formData.wkType) || '',
+ feildValue: normalizeText(formData.feildValue) || '',
+ ...(formData.len !== void 0 && formData.len !== null && formData.len !== ''
+ ? { len: Number(formData.len) }
+ : {}),
+ ...(formData.lenStr !== void 0 && formData.lenStr !== null && formData.lenStr !== ''
+ ? { lenStr: Number(formData.lenStr) }
+ : {}),
+ ...(formData.sort !== void 0 && formData.sort !== null && formData.sort !== ''
+ ? { sort: Number(formData.sort) }
+ : {}),
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildSerialRuleItemSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ ruleId:
+ params.ruleId !== void 0 && params.ruleId !== null && params.ruleId !== ''
+ ? Number(params.ruleId)
+ : void 0,
+ wkType: normalizeText(params.wkType),
+ feildValue: normalizeText(params.feildValue),
+ len:
+ params.len !== void 0 && params.len !== null && params.len !== ''
+ ? Number(params.len)
+ : void 0,
+ lenStr:
+ params.lenStr !== void 0 && params.lenStr !== null && params.lenStr !== ''
+ ? Number(params.lenStr)
+ : void 0,
+ sort:
+ params.sort !== void 0 && params.sort !== null && params.sort !== ''
+ ? Number(params.sort)
+ : void 0,
+ status:
+ params.status !== void 0 && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function fetchSerialRuleItemPage(params = {}) {
+ return request.post({
+ url: '/serialRuleItem/page',
+ params: buildSerialRuleItemPageParams(params)
+ })
+}
+
+export function fetchGetSerialRuleItemDetail(id) {
+ return request.get({
+ url: `/serialRuleItem/${id}`
+ })
+}
+
+export function fetchGetSerialRuleItemMany(ids) {
+ return request.post({
+ url: `/serialRuleItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveSerialRuleItem(params = {}) {
+ return request.post({
+ url: '/serialRuleItem/save',
+ params: buildSerialRuleItemSavePayload(params)
+ })
+}
+
+export function fetchUpdateSerialRuleItem(params = {}) {
+ return request.post({
+ url: '/serialRuleItem/update',
+ params: buildSerialRuleItemSavePayload(params)
+ })
+}
+
+export function fetchDeleteSerialRuleItem(ids) {
+ return request.post({
+ url: `/serialRuleItem/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSerialRuleItemQuery(condition = '') {
+ return request.post({
+ url: '/serialRuleItem/query',
+ params: {
+ condition: normalizeText(condition)
+ }
+ })
+}
+
+export async function fetchExportSerialRuleItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/serialRuleItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/statistic-count.js b/rsf-design/src/api/statistic-count.js
new file mode 100644
index 0000000..00aec66
--- /dev/null
+++ b/rsf-design/src/api/statistic-count.js
@@ -0,0 +1,33 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+export function buildStatisticCountPageParams(params = {}) {
+ const entries = Object.entries(params).filter(([key, value]) => {
+ if (['current', 'pageSize', 'size'].includes(key)) {
+ return false
+ }
+ if (value === undefined || value === null) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...Object.fromEntries(entries.map(([key, value]) => [key, normalizeText(value)]))
+ }
+}
+
+export function fetchStatisticCountPage(params = {}) {
+ return request.post({
+ url: '/statistic/num/page',
+ params: buildStatisticCountPageParams(params)
+ })
+}
diff --git a/rsf-design/src/api/stock-item.js b/rsf-design/src/api/stock-item.js
new file mode 100644
index 0000000..87e8c1e
--- /dev/null
+++ b/rsf-design/src/api/stock-item.js
@@ -0,0 +1,82 @@
+import request from '@/utils/http'
+import {
+ buildStockItemPageQueryParams,
+ buildStockItemSavePayload,
+ buildStockItemSearchParams
+} from '@/views/manager/stock-item/stockItemPage.helpers.js'
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+export function fetchStockItemPage(params = {}) {
+ return request.post({
+ url: '/stockItem/page',
+ params: buildStockItemPageQueryParams(params)
+ })
+}
+
+export function fetchStockItemList(params = {}) {
+ return request.post({
+ url: '/stockItem/list',
+ data: buildStockItemSearchParams(params)
+ })
+}
+
+export function fetchStockItemDetail(id) {
+ return request.get({
+ url: `/stockItem/${id}`
+ })
+}
+
+export function fetchStockItemMany(ids) {
+ return request.post({
+ url: `/stockItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchStockItemQuery(condition = '') {
+ return request.post({
+ url: '/stockItem/query',
+ params: {
+ condition
+ }
+ })
+}
+
+export function fetchSaveStockItem(params = {}) {
+ return request.post({
+ url: '/stockItem/save',
+ params: buildStockItemSavePayload(params)
+ })
+}
+
+export function fetchUpdateStockItem(params = {}) {
+ return request.post({
+ url: '/stockItem/update',
+ params: buildStockItemSavePayload(params)
+ })
+}
+
+export function fetchDeleteStockItem(ids) {
+ return request.post({
+ url: `/stockItem/remove/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportStockItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/stockItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/stock-transfer.js b/rsf-design/src/api/stock-transfer.js
new file mode 100644
index 0000000..530f688
--- /dev/null
+++ b/rsf-design/src/api/stock-transfer.js
@@ -0,0 +1,87 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((item) => String(item).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildStockTransferSourcePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ orderBy: 'create_time desc',
+ locCode: normalizeText(params.locCode || params.orgLoc || ''),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'locCode', 'orgLoc'])
+ }
+}
+
+export function buildStockTransferTargetLocPageParams(params = {}) {
+ const queryParams = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 50,
+ locCode: normalizeText(params.locCode || params.orgLoc || '')
+ }
+ const q = normalizeText(params.q)
+ if (q) {
+ queryParams.q = q
+ }
+ return {
+ ...queryParams,
+ ...filterParams(params, ['current', 'pageSize', 'size', 'locCode', 'orgLoc', 'q'])
+ }
+}
+
+export function buildStockTransferTaskPayload(params = {}) {
+ return {
+ orgLoc: normalizeText(params.orgLoc),
+ tarLoc: normalizeText(params.tarLoc),
+ memo: normalizeText(params.memo)
+ }
+}
+
+export function fetchStockTransferSourcePage(params = {}) {
+ return request.post({ url: '/locItem/useO/page', params: buildStockTransferSourcePageParams(params) })
+}
+
+export function fetchStockTransferSourceDetail(id) {
+ return request.get({ url: `/locItem/${id}` })
+}
+
+export function fetchStockTransferTargetLocPage(params = {}) {
+ return request.post({ url: '/loc/areaNoUse/page', params: buildStockTransferTargetLocPageParams(params) })
+}
+
+export function fetchStockTransferMoveTask(params = {}) {
+ return request.post({ url: '/locItem/move/task', params: buildStockTransferTaskPayload(params) })
+}
+
+export function fetchStockTransferEnabledFields() {
+ return request.get({ url: '/fields/enable/list' })
+}
+
+export { normalizeIds }
diff --git a/rsf-design/src/api/stock.js b/rsf-design/src/api/stock.js
new file mode 100644
index 0000000..8b9c39c
--- /dev/null
+++ b/rsf-design/src/api/stock.js
@@ -0,0 +1,66 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildStockPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchStockPage(params = {}) {
+ return request.post({
+ url: '/stock/page',
+ params: buildStockPageParams(params)
+ })
+}
+
+export function fetchGetStockDetail(id) {
+ return request.get({
+ url: `/stock/${id}`
+ })
+}
+
+export function fetchGetStockMany(ids) {
+ return request.post({
+ url: `/stock/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportStockReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/stock/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/subsystem-flow-template.js b/rsf-design/src/api/subsystem-flow-template.js
new file mode 100644
index 0000000..574947a
--- /dev/null
+++ b/rsf-design/src/api/subsystem-flow-template.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildSubsystemFlowTemplatePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchSubsystemFlowTemplatePage(params = {}) {
+ return request.post({
+ url: '/subsystemFlowTemplate/page',
+ params: buildSubsystemFlowTemplatePageParams(params)
+ })
+}
+
+export function fetchGetSubsystemFlowTemplateDetail(id) {
+ return request.get({
+ url: `/subsystemFlowTemplate/${id}`
+ })
+}
+
+export function fetchGetSubsystemFlowTemplateMany(ids) {
+ return request.post({
+ url: `/subsystemFlowTemplate/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportSubsystemFlowTemplateReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/subsystemFlowTemplate/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/system-manage.js b/rsf-design/src/api/system-manage.js
index f28243d..a9b0a8d 100644
--- a/rsf-design/src/api/system-manage.js
+++ b/rsf-design/src/api/system-manage.js
@@ -24,6 +24,161 @@
}
}
+export function buildOperationRecordPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.namespace !== undefined ? { namespace: params.namespace } : {}),
+ ...(params.url !== undefined ? { url: params.url } : {}),
+ ...(params.appkey !== undefined ? { appkey: params.appkey } : {}),
+ ...(params.clientIp !== undefined ? { clientIp: params.clientIp } : {}),
+ ...(params.request !== undefined ? { request: params.request } : {}),
+ ...(params.response !== undefined ? { response: params.response } : {}),
+ ...(params.spendTime !== undefined ? { spendTime: params.spendTime } : {}),
+ ...(params.result !== undefined ? { result: params.result } : {}),
+ ...(params.userId !== undefined ? { userId: params.userId } : {}),
+ ...(params.timeStart !== undefined ? { timeStart: params.timeStart } : {}),
+ ...(params.timeEnd !== undefined ? { timeEnd: params.timeEnd } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildConfigPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.uuid !== undefined ? { uuid: params.uuid } : {}),
+ ...(params.name !== undefined ? { name: params.name } : {}),
+ ...(params.flag !== undefined ? { flag: params.flag } : {}),
+ ...(params.type !== undefined ? { type: params.type } : {}),
+ ...(params.val !== undefined ? { val: params.val } : {}),
+ ...(params.content !== undefined ? { content: params.content } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.timeStart !== undefined ? { timeStart: params.timeStart } : {}),
+ ...(params.timeEnd !== undefined ? { timeEnd: params.timeEnd } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildSerialRulePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.code !== undefined ? { code: params.code } : {}),
+ ...(params.name !== undefined ? { name: params.name } : {}),
+ ...(params.delimit !== undefined ? { delimit: params.delimit } : {}),
+ ...(params.reset !== undefined ? { reset: params.reset } : {}),
+ ...(params.resetDep !== undefined ? { resetDep: params.resetDep } : {}),
+ ...(params.currValue !== undefined ? { currValue: params.currValue } : {}),
+ ...(params.lastCode !== undefined ? { lastCode: params.lastCode } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.timeStart !== undefined ? { timeStart: params.timeStart } : {}),
+ ...(params.timeEnd !== undefined ? { timeEnd: params.timeEnd } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildDictTypePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.code !== undefined ? { code: params.code } : {}),
+ ...(params.name !== undefined ? { name: params.name } : {}),
+ ...(params.description !== undefined ? { description: params.description } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.timeStart !== undefined ? { timeStart: params.timeStart } : {}),
+ ...(params.timeEnd !== undefined ? { timeEnd: params.timeEnd } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildDictDataPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.dictTypeId !== undefined ? { dictTypeId: params.dictTypeId } : {}),
+ ...(params.dictTypeCode !== undefined ? { dictTypeCode: params.dictTypeCode } : {}),
+ ...(params.value !== undefined ? { value: params.value } : {}),
+ ...(params.label !== undefined ? { label: params.label } : {}),
+ ...(params.sort !== undefined ? { sort: params.sort } : {}),
+ ...(params.group !== undefined ? { group: params.group } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildWaveRulePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.code !== undefined ? { code: params.code } : {}),
+ ...(params.type !== undefined ? { type: params.type } : {}),
+ ...(params.name !== undefined ? { name: params.name } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.timeStart !== undefined ? { timeStart: params.timeStart } : {}),
+ ...(params.timeEnd !== undefined ? { timeEnd: params.timeEnd } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildFieldsPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.fields !== undefined ? { fields: params.fields } : {}),
+ ...(params.fieldsAlise !== undefined ? { fieldsAlise: params.fieldsAlise } : {}),
+ ...(params.unique !== undefined ? { unique: params.unique } : {}),
+ ...(params.flagEnable !== undefined ? { flagEnable: params.flagEnable } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildFieldsItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.uuid !== undefined ? { uuid: params.uuid } : {}),
+ ...(params.fieldsId !== undefined ? { fieldsId: params.fieldsId } : {}),
+ ...(params.value !== undefined ? { value: params.value } : {}),
+ ...(params.matnrId !== undefined ? { matnrId: params.matnrId } : {}),
+ ...(params.shiperId !== undefined ? { shiperId: params.shiperId } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildTenantPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.name !== undefined ? { name: params.name } : {}),
+ ...(params.flag !== undefined ? { flag: params.flag } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
+export function buildHostPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.condition !== undefined ? { condition: params.condition } : {}),
+ ...(params.name !== undefined ? { name: params.name } : {}),
+ ...(params.status !== undefined ? { status: params.status } : {}),
+ ...(params.memo !== undefined ? { memo: params.memo } : {})
+ }
+}
+
function fetchGetUserList(params) {
return request.post({ url: '/user/page', params: buildUserListParams(params) })
}
@@ -55,6 +210,208 @@
function fetchGetRoleList(params) {
return request.post({ url: '/role/page', params: buildRoleListParams(params) })
+}
+
+function fetchOperationRecordPage(params = {}) {
+ return request.post({ url: '/operationRecord/page', params: buildOperationRecordPageParams(params) })
+}
+
+function fetchConfigPage(params = {}) {
+ return request.post({ url: '/config/page', params: buildConfigPageParams(params) })
+}
+
+function fetchSerialRulePage(params = {}) {
+ return request.post({ url: '/serialRule/page', params: buildSerialRulePageParams(params) })
+}
+
+function fetchDictTypePage(params = {}) {
+ return request.post({ url: '/dictType/page', params: buildDictTypePageParams(params) })
+}
+
+function fetchDictDataPage(params = {}) {
+ return request.post({ url: '/dictData/page', params: buildDictDataPageParams(params) })
+}
+
+function fetchGetDictTypeDetail(id) {
+ return request.get({ url: `/dictType/${id}` })
+}
+
+function fetchSaveDictType(params) {
+ return request.post({ url: '/dictType/save', params })
+}
+
+function fetchUpdateDictType(params) {
+ return request.post({ url: '/dictType/update', params })
+}
+
+function fetchDeleteDictType(id) {
+ return request.post({ url: `/dictType/remove/${id}` })
+}
+
+function fetchWaveRulePage(params = {}) {
+ return request.post({ url: '/waveRule/page', params: buildWaveRulePageParams(params) })
+}
+
+function fetchGetWaveRuleDetail(id) {
+ return request.get({ url: `/waveRule/${id}` })
+}
+
+function fetchSaveWaveRule(params) {
+ return request.post({ url: '/waveRule/save', params })
+}
+
+function fetchUpdateWaveRule(params) {
+ return request.post({ url: '/waveRule/update', params })
+}
+
+function fetchDeleteWaveRule(id) {
+ return request.post({ url: `/waveRule/remove/${id}` })
+}
+
+function fetchFieldsPage(params = {}) {
+ return request.post({ url: '/fields/page', params: buildFieldsPageParams(params) })
+}
+
+function fetchGetFieldsDetail(id) {
+ return request.get({ url: `/fields/${id}` })
+}
+
+function fetchSaveFields(params) {
+ return request.post({ url: '/fields/save', params })
+}
+
+function fetchUpdateFields(params) {
+ return request.post({ url: '/fields/update', params })
+}
+
+function fetchDeleteFields(id) {
+ return request.post({ url: `/fields/remove/${id}` })
+}
+
+function fetchFieldsItemPage(params = {}) {
+ return request.post({ url: '/fieldsItem/page', params: buildFieldsItemPageParams(params) })
+}
+
+function fetchGetFieldsItemDetail(id) {
+ return request.get({ url: `/fieldsItem/${id}` })
+}
+
+function fetchSaveFieldsItem(params) {
+ return request.post({ url: '/fieldsItem/save', params })
+}
+
+function fetchUpdateFieldsItem(params) {
+ return request.post({ url: '/fieldsItem/update', params })
+}
+
+function fetchDeleteFieldsItem(id) {
+ return request.post({ url: `/fieldsItem/remove/${id}` })
+}
+
+function fetchTenantPage(params = {}) {
+ return request.post({ url: '/tenant/page', params: buildTenantPageParams(params) })
+}
+
+function fetchGetTenantDetail(id) {
+ return request.get({ url: `/tenant/${id}` })
+}
+
+function fetchInitTenant(params) {
+ return request.post({ url: '/tenant/init', params })
+}
+
+function fetchUpdateTenant(params) {
+ return request.post({ url: '/tenant/update', params })
+}
+
+function fetchDeleteTenant(id) {
+ return request.post({ url: `/tenant/remove/${id}` })
+}
+
+function fetchHostPage(params = {}) {
+ return request.post({ url: '/host/page', params: buildHostPageParams(params) })
+}
+
+function fetchGetHostDetail(id) {
+ return request.get({ url: `/host/${id}` })
+}
+
+function fetchSaveHost(params) {
+ return request.post({ url: '/host/save', params })
+}
+
+function fetchUpdateHost(params) {
+ return request.post({ url: '/host/update', params })
+}
+
+function fetchDeleteHost(id) {
+ return request.post({ url: `/host/remove/${id}` })
+}
+
+function fetchGetSerialRuleDetail(id) {
+ return request.get({ url: `/serialRule/${id}` })
+}
+
+function fetchSaveSerialRule(params) {
+ return request.post({ url: '/serialRule/save', params })
+}
+
+function fetchUpdateSerialRule(params) {
+ return request.post({ url: '/serialRule/update', params })
+}
+
+function fetchDeleteSerialRule(id) {
+ return request.post({ url: `/serialRule/remove/${id}` })
+}
+
+function fetchGetConfigDetail(id) {
+ return request.get({ url: `/config/${id}` })
+}
+
+function fetchSaveConfig(params) {
+ return request.post({ url: '/config/save', params })
+}
+
+function fetchUpdateConfig(params) {
+ return request.post({ url: '/config/update', params })
+}
+
+function fetchDeleteConfig(id) {
+ return request.post({ url: `/config/remove/${id}` })
+}
+
+function fetchGetOperationRecordDetail(id) {
+ return request.get({ url: `/operationRecord/${id}` })
+}
+
+function fetchDeleteOperationRecord(id) {
+ return request.post({ url: `/operationRecord/remove/${id}` })
+}
+
+function normalizeOperationRecordManyIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => normalizeLegacyId(id))
+ .filter((id) => id !== '')
+ .join(',')
+ }
+ return normalizeLegacyId(ids)
+}
+
+function fetchGetOperationRecordMany(ids) {
+ const normalizedIds = normalizeOperationRecordManyIds(ids)
+ return request.post({ url: `/operationRecord/many/${normalizedIds}` })
+}
+
+async function fetchExportOperationRecordReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/operationRecord/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
}
function fetchSaveRole(params) {
@@ -121,6 +478,22 @@
return request.post({ url: '/dept/tree', params })
}
+function fetchGetDeptDetail(id) {
+ return request.get({ url: `/dept/${id}` })
+}
+
+function fetchSaveDept(params) {
+ return request.post({ url: '/dept/save', params })
+}
+
+function fetchUpdateDept(params) {
+ return request.post({ url: '/dept/update', params })
+}
+
+function fetchDeleteDept(id) {
+ return request.post({ url: `/dept/remove/${id}` })
+}
+
function fetchGetMenuTree(params = {}) {
return request.post({ url: '/menu/tree', params })
}
@@ -135,6 +508,22 @@
function fetchDeleteMenu(id) {
return request.post({ url: `/menu/remove/${id}` })
+}
+
+function fetchGetMenuPdaTree(params = {}) {
+ return request.post({ url: '/menuPda/tree', params })
+}
+
+function fetchSaveMenuPda(params) {
+ return request.post({ url: '/menuPda/save', params })
+}
+
+function fetchUpdateMenuPda(params) {
+ return request.post({ url: '/menuPda/update', params })
+}
+
+function fetchDeleteMenuPda(id) {
+ return request.post({ url: `/menuPda/remove/${id}` })
}
function assertAdminPasswordUpdatePayload(params) {
@@ -468,6 +857,52 @@
fetchUpdateUserStatus,
fetchGetUserDetail,
fetchGetRoleList,
+ fetchOperationRecordPage,
+ fetchGetOperationRecordDetail,
+ fetchDeleteOperationRecord,
+ fetchGetOperationRecordMany,
+ fetchExportOperationRecordReport,
+ fetchConfigPage,
+ fetchGetConfigDetail,
+ fetchSaveConfig,
+ fetchUpdateConfig,
+ fetchDeleteConfig,
+ fetchSerialRulePage,
+ fetchGetSerialRuleDetail,
+ fetchSaveSerialRule,
+ fetchUpdateSerialRule,
+ fetchDeleteSerialRule,
+ fetchDictTypePage,
+ fetchGetDictTypeDetail,
+ fetchSaveDictType,
+ fetchUpdateDictType,
+ fetchDeleteDictType,
+ fetchDictDataPage,
+ fetchWaveRulePage,
+ fetchGetWaveRuleDetail,
+ fetchSaveWaveRule,
+ fetchUpdateWaveRule,
+ fetchDeleteWaveRule,
+ fetchFieldsPage,
+ fetchGetFieldsDetail,
+ fetchSaveFields,
+ fetchUpdateFields,
+ fetchDeleteFields,
+ fetchFieldsItemPage,
+ fetchGetFieldsItemDetail,
+ fetchSaveFieldsItem,
+ fetchUpdateFieldsItem,
+ fetchDeleteFieldsItem,
+ fetchTenantPage,
+ fetchGetTenantDetail,
+ fetchInitTenant,
+ fetchUpdateTenant,
+ fetchDeleteTenant,
+ fetchHostPage,
+ fetchGetHostDetail,
+ fetchSaveHost,
+ fetchUpdateHost,
+ fetchDeleteHost,
fetchSaveRole,
fetchUpdateRole,
fetchDeleteRole,
@@ -477,10 +912,18 @@
fetchExportRoleReport,
fetchGetRoleMany,
fetchGetDeptTree,
+ fetchGetDeptDetail,
+ fetchSaveDept,
+ fetchUpdateDept,
+ fetchDeleteDept,
fetchGetMenuTree,
+ fetchGetMenuPdaTree,
fetchSaveMenu,
+ fetchSaveMenuPda,
fetchUpdateMenu,
+ fetchUpdateMenuPda,
fetchDeleteMenu,
+ fetchDeleteMenuPda,
fetchGetRoleScopeList,
fetchGetRoleScopeTree,
fetchUpdateRoleScope,
diff --git a/rsf-design/src/api/task-instance-node.js b/rsf-design/src/api/task-instance-node.js
new file mode 100644
index 0000000..9ef6dae
--- /dev/null
+++ b/rsf-design/src/api/task-instance-node.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildTaskInstanceNodePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchTaskInstanceNodePage(params = {}) {
+ return request.post({
+ url: '/taskInstanceNode/page',
+ params: buildTaskInstanceNodePageParams(params)
+ })
+}
+
+export function fetchGetTaskInstanceNodeDetail(id) {
+ return request.get({
+ url: `/taskInstanceNode/${id}`
+ })
+}
+
+export function fetchGetTaskInstanceNodeMany(ids) {
+ return request.post({
+ url: `/taskInstanceNode/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportTaskInstanceNodeReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/taskInstanceNode/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/task-instance.js b/rsf-design/src/api/task-instance.js
new file mode 100644
index 0000000..bec15c7
--- /dev/null
+++ b/rsf-design/src/api/task-instance.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildTaskInstancePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchTaskInstancePage(params = {}) {
+ return request.post({
+ url: '/taskInstance/page',
+ params: buildTaskInstancePageParams(params)
+ })
+}
+
+export function fetchGetTaskInstanceDetail(id) {
+ return request.get({
+ url: `/taskInstance/${id}`
+ })
+}
+
+export function fetchGetTaskInstanceMany(ids) {
+ return request.post({
+ url: `/taskInstance/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportTaskInstanceReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/taskInstance/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/task-item-log.js b/rsf-design/src/api/task-item-log.js
new file mode 100644
index 0000000..aee4273
--- /dev/null
+++ b/rsf-design/src/api/task-item-log.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildTaskItemLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchTaskItemLogPage(params = {}) {
+ return request.post({
+ url: '/taskItemLog/page',
+ params: buildTaskItemLogPageParams(params)
+ })
+}
+
+export function fetchGetTaskItemLogDetail(id) {
+ return request.get({
+ url: `/taskItemLog/${id}`
+ })
+}
+
+export function fetchGetTaskItemLogMany(ids) {
+ return request.post({
+ url: `/taskItemLog/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportTaskItemLogReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/taskItemLog/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/task-item.js b/rsf-design/src/api/task-item.js
new file mode 100644
index 0000000..a0df397
--- /dev/null
+++ b/rsf-design/src/api/task-item.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildTaskItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchTaskItemPage(params = {}) {
+ return request.post({
+ url: '/taskItem/page',
+ params: buildTaskItemPageParams(params)
+ })
+}
+
+export function fetchGetTaskItemDetail(id) {
+ return request.get({
+ url: `/taskItem/${id}`
+ })
+}
+
+export function fetchGetTaskItemMany(ids) {
+ return request.post({
+ url: `/taskItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportTaskItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/taskItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/task-log.js b/rsf-design/src/api/task-log.js
new file mode 100644
index 0000000..342f9ac
--- /dev/null
+++ b/rsf-design/src/api/task-log.js
@@ -0,0 +1,69 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildTaskLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchTaskLogPage(params = {}) {
+ return request.post({
+ url: '/taskLog/page',
+ params: buildTaskLogPageParams(params)
+ })
+}
+
+export function fetchGetTaskLogDetail(id) {
+ return request.get({
+ url: `/taskLog/${id}`
+ })
+}
+
+export function fetchGetTaskLogMany(ids) {
+ return request.post({
+ url: `/taskLog/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportTaskLogReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/taskLog/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/task-path-template-merge.js b/rsf-design/src/api/task-path-template-merge.js
new file mode 100644
index 0000000..614b13d
--- /dev/null
+++ b/rsf-design/src/api/task-path-template-merge.js
@@ -0,0 +1,298 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+function normalizeStringList(value) {
+ if (Array.isArray(value)) {
+ return value.map((item) => normalizeText(item)).filter(Boolean)
+ }
+ if (value === null || value === undefined || value === '') {
+ return []
+ }
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text) {
+ return []
+ }
+ if (text.startsWith('[')) {
+ try {
+ const parsed = JSON.parse(text)
+ if (Array.isArray(parsed)) {
+ return parsed.map((item) => normalizeText(item)).filter(Boolean)
+ }
+ } catch {
+ return [text]
+ }
+ }
+ return text.split(/[;,锛孿n\r]+/g).map((item) => item.trim()).filter(Boolean)
+ }
+ return [normalizeText(value)].filter(Boolean)
+}
+
+function normalizeIntegerList(value) {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => {
+ if (item === null || item === undefined || item === '') {
+ return null
+ }
+ const numberValue = Number(item)
+ return Number.isNaN(numberValue) ? null : numberValue
+ })
+ .filter((item) => item !== null)
+ }
+ if (value === null || value === undefined || value === '') {
+ return []
+ }
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text) {
+ return []
+ }
+ if (text.startsWith('[')) {
+ try {
+ const parsed = JSON.parse(text)
+ if (Array.isArray(parsed)) {
+ return parsed
+ .map((item) => {
+ const numberValue = Number(item)
+ return Number.isNaN(numberValue) ? null : numberValue
+ })
+ .filter((item) => item !== null)
+ }
+ } catch {
+ return text
+ .split(/[;,锛孿n\r]+/g)
+ .map((item) => Number(item.trim()))
+ .filter((item) => !Number.isNaN(item))
+ }
+ }
+ const numberValue = Number(text)
+ return Number.isNaN(numberValue) ? [] : [numberValue]
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? [] : [numberValue]
+}
+
+export function buildTaskPathTemplateMergePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildTaskPathTemplateMergeSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ templateCode: normalizeText(params.templateCode),
+ templateName: normalizeText(params.templateName),
+ sourceType: normalizeText(params.sourceType),
+ targetType: normalizeText(params.targetType),
+ conditionExpression: normalizeText(params.conditionExpression),
+ conditionDesc: normalizeText(params.conditionDesc),
+ version:
+ params.version !== undefined && params.version !== null && params.version !== ''
+ ? Number(params.version)
+ : void 0,
+ isCurrent:
+ params.isCurrent !== undefined && params.isCurrent !== null && params.isCurrent !== ''
+ ? Number(params.isCurrent)
+ : void 0,
+ priority:
+ params.priority !== undefined && params.priority !== null && params.priority !== ''
+ ? Number(params.priority)
+ : void 0,
+ timeoutMinutes:
+ params.timeoutMinutes !== undefined && params.timeoutMinutes !== null && params.timeoutMinutes !== ''
+ ? Number(params.timeoutMinutes)
+ : void 0,
+ maxRetryTimes:
+ params.maxRetryTimes !== undefined && params.maxRetryTimes !== null && params.maxRetryTimes !== ''
+ ? Number(params.maxRetryTimes)
+ : void 0,
+ retryIntervalSeconds:
+ params.retryIntervalSeconds !== undefined && params.retryIntervalSeconds !== null && params.retryIntervalSeconds !== ''
+ ? Number(params.retryIntervalSeconds)
+ : void 0,
+ stepSize:
+ params.stepSize !== undefined && params.stepSize !== null && params.stepSize !== ''
+ ? Number(params.stepSize)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ effectiveTime: normalizeText(params.effectiveTime),
+ expireTime: normalizeText(params.expireTime),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildTaskPathTemplateMergeSavePayload(formData = {}) {
+ const isEditMode = formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ const sourceTypeR = normalizeStringList(formData.sourceTypeR)
+ const targetTypeR = normalizeStringList(formData.targetTypeR)
+ const sourceType = normalizeText(formData.sourceType || '')
+ const targetType = normalizeText(formData.targetType || '')
+ const conditionExpression = normalizeIntegerList(formData.conditionExpression)
+ const derivedSource = sourceType || sourceTypeR[0] || ''
+ const derivedTarget = targetType || targetTypeR[0] || ''
+ const templateName = normalizeText(formData.templateName || '') || (derivedSource && derivedTarget ? `${derivedSource}==>${derivedTarget}` : '')
+ const templateCode = normalizeText(formData.templateCode || '') || templateName
+
+ const payload = {
+ ...(isEditMode ? { id: Number(formData.id) } : {}),
+ templateCode,
+ templateName,
+ conditionExpression,
+ conditionDesc: normalizeText(formData.conditionDesc || ''),
+ version:
+ formData.version !== void 0 && formData.version !== null && formData.version !== ''
+ ? Number(formData.version)
+ : 1,
+ isCurrent:
+ formData.isCurrent !== void 0 && formData.isCurrent !== null && formData.isCurrent !== ''
+ ? Number(formData.isCurrent)
+ : 1,
+ effectiveTime: normalizeText(formData.effectiveTime || ''),
+ expireTime: normalizeText(formData.expireTime || ''),
+ priority:
+ formData.priority !== void 0 && formData.priority !== null && formData.priority !== ''
+ ? Number(formData.priority)
+ : 1,
+ ...(formData.timeoutMinutes !== void 0 && formData.timeoutMinutes !== null && formData.timeoutMinutes !== ''
+ ? { timeoutMinutes: Number(formData.timeoutMinutes) }
+ : {}),
+ maxRetryTimes:
+ formData.maxRetryTimes !== void 0 && formData.maxRetryTimes !== null && formData.maxRetryTimes !== ''
+ ? Number(formData.maxRetryTimes)
+ : 3,
+ retryIntervalSeconds:
+ formData.retryIntervalSeconds !== void 0 &&
+ formData.retryIntervalSeconds !== null &&
+ formData.retryIntervalSeconds !== ''
+ ? Number(formData.retryIntervalSeconds)
+ : 60,
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ ...(formData.stepSize !== void 0 && formData.stepSize !== null && formData.stepSize !== ''
+ ? { stepSize: Number(formData.stepSize) }
+ : {})
+ }
+
+ if (isEditMode) {
+ payload.sourceType = sourceType
+ payload.targetType = targetType
+ } else {
+ payload.sourceTypeR = sourceTypeR
+ payload.targetTypeR = targetTypeR
+ }
+
+ return payload
+}
+
+export function fetchTaskPathTemplateMergeCreateSelectList() {
+ return request.post({
+ url: '/taskPathTemplateMerge/createSelectList'
+ })
+}
+
+export function fetchTaskPathTemplateMergePage(params = {}) {
+ return request.post({
+ url: '/taskPathTemplateMerge/page',
+ params: buildTaskPathTemplateMergePageParams(params)
+ })
+}
+
+export function fetchTaskPathTemplateMergeList(params = {}) {
+ return request.post({
+ url: '/taskPathTemplateMerge/list',
+ params: filterParams(params)
+ })
+}
+
+export function fetchGetTaskPathTemplateMergeDetail(id) {
+ return request.get({
+ url: `/taskPathTemplateMerge/${id}`
+ })
+}
+
+export function fetchGetTaskPathTemplateMergeMany(ids) {
+ return request.post({
+ url: `/taskPathTemplateMerge/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveTaskPathTemplateMerge(params = {}) {
+ return request.post({
+ url: '/taskPathTemplateMerge/save',
+ params: buildTaskPathTemplateMergeSavePayload(params)
+ })
+}
+
+export function fetchUpdateTaskPathTemplateMerge(params = {}) {
+ return request.post({
+ url: '/taskPathTemplateMerge/update',
+ params: buildTaskPathTemplateMergeSavePayload(params)
+ })
+}
+
+export function fetchDeleteTaskPathTemplateMerge(ids) {
+ return request.post({
+ url: `/taskPathTemplateMerge/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchTaskPathTemplateMergeQuery(condition = '') {
+ return request.post({
+ url: '/taskPathTemplateMerge/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportTaskPathTemplateMergeReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/taskPathTemplateMerge/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/task-path-template-node.js b/rsf-design/src/api/task-path-template-node.js
new file mode 100644
index 0000000..783f0bb
--- /dev/null
+++ b/rsf-design/src/api/task-path-template-node.js
@@ -0,0 +1,175 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+function toNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return void 0
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? void 0 : numberValue
+}
+
+export function buildTaskPathTemplateNodePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildTaskPathTemplateNodeSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ templateId: toNumber(params.templateId),
+ templateCode: normalizeText(params.templateCode),
+ nodeOrder: toNumber(params.nodeOrder),
+ nodeCode: normalizeText(params.nodeCode),
+ nodeName: normalizeText(params.nodeName),
+ nodeType: normalizeText(params.nodeType),
+ systemCode: normalizeText(params.systemCode),
+ systemName: normalizeText(params.systemName),
+ executeParams: normalizeText(params.executeParams),
+ resultSchema: normalizeText(params.resultSchema),
+ timeoutMinutes: toNumber(params.timeoutMinutes),
+ mandatory: toNumber(params.mandatory),
+ parallelExecutable: toNumber(params.parallelExecutable),
+ preCondition: normalizeText(params.preCondition),
+ postCondition: normalizeText(params.postCondition),
+ nextNodeRules: normalizeText(params.nextNodeRules),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildTaskPathTemplateNodeSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.templateId !== undefined && formData.templateId !== null && formData.templateId !== ''
+ ? { templateId: Number(formData.templateId) }
+ : {}),
+ templateCode: normalizeText(formData.templateCode) || '',
+ ...(formData.nodeOrder !== undefined && formData.nodeOrder !== null && formData.nodeOrder !== ''
+ ? { nodeOrder: Number(formData.nodeOrder) }
+ : {}),
+ nodeCode: normalizeText(formData.nodeCode) || '',
+ nodeName: normalizeText(formData.nodeName) || '',
+ nodeType: normalizeText(formData.nodeType) || 'EXECUTE',
+ systemCode: normalizeText(formData.systemCode) || '',
+ systemName: normalizeText(formData.systemName) || '',
+ executeParams: normalizeText(formData.executeParams) || '',
+ resultSchema: normalizeText(formData.resultSchema) || '',
+ ...(formData.timeoutMinutes !== undefined && formData.timeoutMinutes !== null && formData.timeoutMinutes !== ''
+ ? { timeoutMinutes: Number(formData.timeoutMinutes) }
+ : {}),
+ ...(formData.mandatory !== undefined && formData.mandatory !== null && formData.mandatory !== ''
+ ? { mandatory: Number(formData.mandatory) }
+ : { mandatory: 1 }),
+ ...(formData.parallelExecutable !== undefined &&
+ formData.parallelExecutable !== null &&
+ formData.parallelExecutable !== ''
+ ? { parallelExecutable: Number(formData.parallelExecutable) }
+ : { parallelExecutable: 0 }),
+ preCondition: normalizeText(formData.preCondition) || '',
+ postCondition: normalizeText(formData.postCondition) || '',
+ nextNodeRules: normalizeText(formData.nextNodeRules) || ''
+ }
+}
+
+export function fetchTaskPathTemplateNodePage(params = {}) {
+ return request.post({
+ url: '/taskPathTemplateNode/page',
+ params: buildTaskPathTemplateNodePageParams(params)
+ })
+}
+
+export function fetchTaskPathTemplateNodeList(params = {}) {
+ return request.post({
+ url: '/taskPathTemplateNode/list',
+ params: filterParams(params)
+ })
+}
+
+export function fetchGetTaskPathTemplateNodeDetail(id) {
+ return request.get({
+ url: `/taskPathTemplateNode/${id}`
+ })
+}
+
+export function fetchGetTaskPathTemplateNodeMany(ids) {
+ return request.post({
+ url: `/taskPathTemplateNode/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveTaskPathTemplateNode(params = {}) {
+ return request.post({
+ url: '/taskPathTemplateNode/save',
+ params: buildTaskPathTemplateNodeSavePayload(params)
+ })
+}
+
+export function fetchUpdateTaskPathTemplateNode(params = {}) {
+ return request.post({
+ url: '/taskPathTemplateNode/update',
+ params: buildTaskPathTemplateNodeSavePayload(params)
+ })
+}
+
+export function fetchDeleteTaskPathTemplateNode(ids) {
+ return request.post({
+ url: `/taskPathTemplateNode/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchTaskPathTemplateNodeQuery(condition = '') {
+ return request.post({
+ url: '/taskPathTemplateNode/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportTaskPathTemplateNodeReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/taskPathTemplateNode/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/task-path-template.js b/rsf-design/src/api/task-path-template.js
new file mode 100644
index 0000000..0bb5885
--- /dev/null
+++ b/rsf-design/src/api/task-path-template.js
@@ -0,0 +1,197 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildTaskPathTemplatePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildTaskPathTemplateSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ templateCode: normalizeText(params.templateCode),
+ templateName: normalizeText(params.templateName),
+ sourceType: normalizeText(params.sourceType),
+ targetType: normalizeText(params.targetType),
+ conditionExpression: normalizeText(params.conditionExpression),
+ conditionDesc: normalizeText(params.conditionDesc),
+ version:
+ params.version !== undefined && params.version !== null && params.version !== ''
+ ? Number(params.version)
+ : void 0,
+ isCurrent:
+ params.isCurrent !== undefined && params.isCurrent !== null && params.isCurrent !== ''
+ ? Number(params.isCurrent)
+ : void 0,
+ priority:
+ params.priority !== undefined && params.priority !== null && params.priority !== ''
+ ? Number(params.priority)
+ : void 0,
+ timeoutMinutes:
+ params.timeoutMinutes !== undefined && params.timeoutMinutes !== null && params.timeoutMinutes !== ''
+ ? Number(params.timeoutMinutes)
+ : void 0,
+ stepSize:
+ params.stepSize !== undefined && params.stepSize !== null && params.stepSize !== ''
+ ? Number(params.stepSize)
+ : void 0,
+ maxRetryTimes:
+ params.maxRetryTimes !== undefined && params.maxRetryTimes !== null && params.maxRetryTimes !== ''
+ ? Number(params.maxRetryTimes)
+ : void 0,
+ retryIntervalSeconds:
+ params.retryIntervalSeconds !== undefined && params.retryIntervalSeconds !== null && params.retryIntervalSeconds !== ''
+ ? Number(params.retryIntervalSeconds)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ remark: normalizeText(params.remark),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildTaskPathTemplateSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ templateCode: normalizeText(formData.templateCode) || '',
+ templateName: normalizeText(formData.templateName) || '',
+ sourceType: normalizeText(formData.sourceType) || '',
+ targetType: normalizeText(formData.targetType) || '',
+ conditionExpression: normalizeText(formData.conditionExpression) || '',
+ conditionDesc: normalizeText(formData.conditionDesc) || '',
+ ...(formData.version !== undefined && formData.version !== null && formData.version !== ''
+ ? { version: Number(formData.version) }
+ : {}),
+ ...(formData.isCurrent !== undefined && formData.isCurrent !== null && formData.isCurrent !== ''
+ ? { isCurrent: Number(formData.isCurrent) }
+ : {}),
+ effectiveTime: normalizeText(formData.effectiveTime) || '',
+ expireTime: normalizeText(formData.expireTime) || '',
+ ...(formData.priority !== undefined && formData.priority !== null && formData.priority !== ''
+ ? { priority: Number(formData.priority) }
+ : {}),
+ ...(formData.timeoutMinutes !== undefined && formData.timeoutMinutes !== null && formData.timeoutMinutes !== ''
+ ? { timeoutMinutes: Number(formData.timeoutMinutes) }
+ : {}),
+ ...(formData.stepSize !== undefined && formData.stepSize !== null && formData.stepSize !== ''
+ ? { stepSize: Number(formData.stepSize) }
+ : {}),
+ ...(formData.maxRetryTimes !== undefined && formData.maxRetryTimes !== null && formData.maxRetryTimes !== ''
+ ? { maxRetryTimes: Number(formData.maxRetryTimes) }
+ : {}),
+ ...(formData.retryIntervalSeconds !== undefined &&
+ formData.retryIntervalSeconds !== null &&
+ formData.retryIntervalSeconds !== ''
+ ? { retryIntervalSeconds: Number(formData.retryIntervalSeconds) }
+ : {}),
+ ...(formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? { status: Number(formData.status) }
+ : {}),
+ remark: normalizeText(formData.remark) || ''
+ }
+}
+
+export function fetchTaskPathTemplatePage(params = {}) {
+ return request.post({
+ url: '/taskPathTemplate/page',
+ params: buildTaskPathTemplatePageParams(params)
+ })
+}
+
+export function fetchTaskPathTemplateList(params = {}) {
+ return request.post({
+ url: '/taskPathTemplate/list',
+ params: filterParams(params)
+ })
+}
+
+export function fetchGetTaskPathTemplateDetail(id) {
+ return request.get({
+ url: `/taskPathTemplate/${id}`
+ })
+}
+
+export function fetchGetTaskPathTemplateMany(ids) {
+ return request.post({
+ url: `/taskPathTemplate/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveTaskPathTemplate(params = {}) {
+ return request.post({
+ url: '/taskPathTemplate/save',
+ params: buildTaskPathTemplateSavePayload(params)
+ })
+}
+
+export function fetchUpdateTaskPathTemplate(params = {}) {
+ return request.post({
+ url: '/taskPathTemplate/update',
+ params: buildTaskPathTemplateSavePayload(params)
+ })
+}
+
+export function fetchDeleteTaskPathTemplate(ids) {
+ return request.post({
+ url: `/taskPathTemplate/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchTaskPathTemplateQuery(condition = '') {
+ return request.post({
+ url: '/taskPathTemplate/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportTaskPathTemplateReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/taskPathTemplate/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/task.js b/rsf-design/src/api/task.js
new file mode 100644
index 0000000..2157d26
--- /dev/null
+++ b/rsf-design/src/api/task.js
@@ -0,0 +1,95 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+export function buildTaskPageParams(params = {}) {
+ const entries = Object.entries(params).filter(([key, value]) => {
+ if (['current', 'pageSize', 'size'].includes(key)) {
+ return false
+ }
+ if (value === undefined || value === null) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...Object.fromEntries(entries.map(([key, value]) => [key, normalizeText(value)]))
+ }
+}
+
+export function buildTaskItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.taskId !== undefined ? { taskId: params.taskId } : {})
+ }
+}
+
+export function fetchTaskPage(params = {}) {
+ return request.post({
+ url: '/task/page',
+ params: buildTaskPageParams(params)
+ })
+}
+
+export function fetchTaskDetail(id) {
+ return request.get({
+ url: `/task/${id}`
+ })
+}
+
+export function fetchTaskItemPage(params = {}) {
+ return request.post({
+ url: '/taskItem/page',
+ params: buildTaskItemPageParams(params)
+ })
+}
+
+export function fetchRemoveTask(ids) {
+ const normalizedIds = normalizeIds(ids)
+ return request.post({
+ url: `/task/remove/${normalizedIds}`
+ })
+}
+
+export function fetchCompleteTask(id) {
+ return request.post({
+ url: `/task/complete/${id}`
+ })
+}
+
+export function fetchPickTask(id) {
+ return request.post({
+ url: `/task/pick/${id}`
+ })
+}
+
+export function fetchCheckTask(id) {
+ return request.post({
+ url: `/task/check/${id}`
+ })
+}
+
+export function fetchTopTask(id) {
+ return request.post({
+ url: `/task/top/${id}`
+ })
+}
diff --git a/rsf-design/src/api/transfer-item.js b/rsf-design/src/api/transfer-item.js
new file mode 100644
index 0000000..6a901ea
--- /dev/null
+++ b/rsf-design/src/api/transfer-item.js
@@ -0,0 +1,125 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+const TEXT_FIELDS = [
+ 'condition',
+ 'timeStart',
+ 'timeEnd',
+ 'transferCode',
+ 'matnrCode',
+ 'maktx',
+ 'unit',
+ 'batch',
+ 'spec',
+ 'model',
+ 'fieldsIndex',
+ 'platItemId',
+ 'platOrderCode',
+ 'platWorkCode',
+ 'projectCode',
+ 'memo'
+]
+
+const NUMBER_FIELDS = ['transferId', 'matnrId', 'anfme', 'workQty', 'qty', 'splrId', 'status']
+
+function buildSearchParams(params = {}) {
+ const result = {}
+
+ TEXT_FIELDS.forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ NUMBER_FIELDS.forEach((key) => {
+ const value = normalizeNumber(params[key], void 0)
+ if (value !== void 0) {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function buildTransferItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildSearchParams(params)
+ }
+}
+
+export function buildTransferItemQueryParams(condition = '') {
+ const normalizedCondition = normalizeText(condition)
+ return normalizedCondition ? { condition: normalizedCondition } : {}
+}
+
+export function fetchTransferItemPage(params = {}) {
+ return request.post({
+ url: '/transferItem/page',
+ params: buildTransferItemPageParams(params)
+ })
+}
+
+export function fetchTransferItemList(params = {}) {
+ return request.post({
+ url: '/transferItem/list',
+ data: buildSearchParams(params)
+ })
+}
+
+export function fetchTransferItemMany(ids) {
+ return request.post({
+ url: `/transferItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchTransferItemDetail(id) {
+ return request.get({
+ url: `/transferItem/${id}`
+ })
+}
+
+export function fetchTransferItemQuery(condition = '') {
+ return request.post({
+ url: '/transferItem/query',
+ params: buildTransferItemQueryParams(condition)
+ })
+}
+
+export async function fetchExportTransferItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/transferItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
+
diff --git a/rsf-design/src/api/transfer.js b/rsf-design/src/api/transfer.js
new file mode 100644
index 0000000..bfb389b
--- /dev/null
+++ b/rsf-design/src/api/transfer.js
@@ -0,0 +1,137 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) return fallback
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) return ''
+ return String(ids).trim()
+}
+
+export function buildTransferSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'orgWareName', 'tarWareName', 'orgAreaName', 'tarAreaName', 'memo', 'timeStart', 'timeEnd'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) result[key] = value
+ })
+ ;['type', 'source', 'exceStatus', 'status', 'orgWareId', 'tarWareId', 'orgAreaId', 'tarAreaId'].forEach((key) => {
+ if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
+ result[key] = normalizeNumber(params[key])
+ }
+ })
+ return result
+}
+
+export function buildTransferPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTransferSearchParams(params)
+ }
+}
+
+export function buildTransferOrderPageParams(params = {}) {
+ return {
+ condition: normalizeText(params.code || params.condition),
+ code: normalizeText(params.code || params.condition),
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function buildTransferSavePayload(formData = {}, areaOptions = []) {
+ const optionMap = new Map(
+ (Array.isArray(areaOptions) ? areaOptions : [])
+ .map((item) => {
+ const value = normalizeNumber(item?.value ?? item?.id, void 0)
+ if (value === void 0) return null
+ return [value, item?.raw || item]
+ })
+ .filter(Boolean)
+ )
+
+ const orgAreaId = normalizeNumber(formData.orgAreaId, void 0)
+ const tarAreaId = normalizeNumber(formData.tarAreaId, void 0)
+ const orgArea = optionMap.get(orgAreaId) || {}
+ const tarArea = optionMap.get(tarAreaId) || {}
+ const orgWareId = normalizeNumber(orgArea.warehouseId ?? orgArea.warehouse_id ?? orgArea.warehouseIdValue, void 0)
+ const tarWareId = normalizeNumber(tarArea.warehouseId ?? tarArea.warehouse_id ?? tarArea.warehouseIdValue, void 0)
+
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: normalizeNumber(formData.id) }
+ : {}),
+ ...(normalizeText(formData.code) ? { code: normalizeText(formData.code) } : {}),
+ ...(formData.type !== undefined && formData.type !== null && formData.type !== ''
+ ? { type: normalizeNumber(formData.type) }
+ : {}),
+ ...(orgAreaId !== void 0 ? { orgAreaId } : {}),
+ ...(tarAreaId !== void 0 ? { tarAreaId } : {}),
+ ...(orgWareId !== void 0 ? { orgWareId } : {}),
+ ...(tarWareId !== void 0 ? { tarWareId } : {}),
+ ...(normalizeText(orgArea.name || orgArea.areaName) ? { orgAreaName: normalizeText(orgArea.name || orgArea.areaName) } : {}),
+ ...(normalizeText(tarArea.name || tarArea.areaName) ? { tarAreaName: normalizeText(tarArea.name || tarArea.areaName) } : {}),
+ ...(normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) ? { orgWareName: normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) } : {}),
+ ...(normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) ? { tarWareName: normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) } : {}),
+ ...(formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? { status: normalizeNumber(formData.status) }
+ : { status: 1 }),
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function fetchTransferPage(params = {}) {
+ return request.post({ url: '/transfer/page', params: buildTransferPageParams(params) })
+}
+
+export function fetchTransferDetail(id) {
+ return request.get({ url: `/transfer/${id}` })
+}
+
+export function fetchTransferMany(ids) {
+ return request.post({ url: `/transfer/many/${normalizeIds(ids)}` })
+}
+
+export function fetchDeleteTransfer(ids) {
+ return request.post({ url: `/transfer/remove/${normalizeIds(ids)}` })
+}
+
+export function fetchSaveTransfer(payload = {}) {
+ return request.post({ url: '/transfer/save', params: payload })
+}
+
+export function fetchUpdateTransfer(payload = {}) {
+ return request.post({ url: '/transfer/update', params: payload })
+}
+
+export function fetchTransferOrdersPage(params = {}) {
+ return request.post({ url: '/transfer/orders/page', params: buildTransferOrderPageParams(params) })
+}
+
+export function fetchTransferPubOutStock(payload = {}) {
+ return request.post({ url: '/transfer/pub/outStock', params: payload })
+}
+
+export async function fetchExportTransferReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/transfer/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/wait-pakin-item-log.js b/rsf-design/src/api/wait-pakin-item-log.js
new file mode 100644
index 0000000..4e72cc2
--- /dev/null
+++ b/rsf-design/src/api/wait-pakin-item-log.js
@@ -0,0 +1,104 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+
+ return String(ids).trim()
+}
+
+function buildSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'asnCode',
+ 'trackCode',
+ 'matnrCode',
+ 'maktx',
+ 'batch',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['logId', 'pakinId', 'pakinItemId', 'asnId', 'asnItemId', 'status'].forEach((key) => {
+ if (params[key] === '' || params[key] === undefined || params[key] === null) {
+ return
+ }
+
+ const numericValue = Number(params[key])
+ if (!Number.isNaN(numericValue)) {
+ result[key] = numericValue
+ }
+ })
+
+ return result
+}
+
+export function buildWaitPakinItemLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildSearchParams(params)
+ }
+}
+
+export function fetchWaitPakinItemLogPage(params = {}) {
+ return request.post({
+ url: '/waitPakinItemLog/page',
+ params: buildWaitPakinItemLogPageParams(params)
+ })
+}
+
+export function fetchWaitPakinItemLogList(params = {}) {
+ return request.post({
+ url: '/waitPakinItemLog/list',
+ data: buildSearchParams(params)
+ })
+}
+
+export function fetchWaitPakinItemLogMany(ids) {
+ return request.post({
+ url: `/waitPakinItemLog/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchWaitPakinItemLogDetail(id) {
+ return request.get({
+ url: `/waitPakinItemLog/${id}`
+ })
+}
+
+export function fetchWaitPakinItemLogQuery(condition = '') {
+ return request.post({
+ url: '/waitPakinItemLog/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportWaitPakinItemLogReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/waitPakinItemLog/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/wait-pakin-item.js b/rsf-design/src/api/wait-pakin-item.js
new file mode 100644
index 0000000..a513e75
--- /dev/null
+++ b/rsf-design/src/api/wait-pakin-item.js
@@ -0,0 +1,105 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+
+ return String(ids).trim()
+}
+
+function buildSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'type',
+ 'asnCode',
+ 'matnrCode',
+ 'trackCode',
+ 'batch',
+ 'memo',
+ 'fieldsIndex'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['pakinId', 'asnItemId', 'wkType', 'isptResult', 'status'].forEach((key) => {
+ if (params[key] === '' || params[key] === undefined || params[key] === null) {
+ return
+ }
+
+ const numericValue = Number(params[key])
+ if (!Number.isNaN(numericValue)) {
+ result[key] = numericValue
+ }
+ })
+
+ return result
+}
+
+export function buildWaitPakinItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildSearchParams(params)
+ }
+}
+
+export function fetchWaitPakinItemPage(params = {}) {
+ return request.post({
+ url: '/waitPakinItem/page',
+ params: buildWaitPakinItemPageParams(params)
+ })
+}
+
+export function fetchWaitPakinItemList(params = {}) {
+ return request.post({
+ url: '/waitPakinItem/list',
+ data: buildSearchParams(params)
+ })
+}
+
+export function fetchWaitPakinItemMany(ids) {
+ return request.post({
+ url: `/waitPakinItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchWaitPakinItemDetail(id) {
+ return request.get({
+ url: `/waitPakinItem/${id}`
+ })
+}
+
+export function fetchWaitPakinItemQuery(condition = '') {
+ return request.post({
+ url: '/waitPakinItem/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export async function fetchExportWaitPakinItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/waitPakinItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/wait-pakin-log.js b/rsf-design/src/api/wait-pakin-log.js
new file mode 100644
index 0000000..8588d51
--- /dev/null
+++ b/rsf-design/src/api/wait-pakin-log.js
@@ -0,0 +1,85 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildWaitPakinLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildWaitPakinItemLogPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.logId !== undefined ? { logId: Number(params.logId) } : {}),
+ ...filterParams(params, ['current', 'pageSize', 'size', 'logId'])
+ }
+}
+
+export function fetchWaitPakinLogPage(params = {}) {
+ return request.post({
+ url: '/waitPakinLog/page',
+ params: buildWaitPakinLogPageParams(params)
+ })
+}
+
+export function fetchGetWaitPakinLogDetail(id) {
+ return request.get({
+ url: `/waitPakinLog/${id}`
+ })
+}
+
+export function fetchGetWaitPakinLogMany(ids) {
+ return request.post({
+ url: `/waitPakinLog/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchWaitPakinItemLogPage(params = {}) {
+ return request.post({
+ url: '/waitPakinItemLog/page',
+ params: buildWaitPakinItemLogPageParams(params)
+ })
+}
+
+export async function fetchExportWaitPakinLogReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/waitPakinLog/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/wait-pakin.js b/rsf-design/src/api/wait-pakin.js
new file mode 100644
index 0000000..2edea99
--- /dev/null
+++ b/rsf-design/src/api/wait-pakin.js
@@ -0,0 +1,111 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildWaitPakinPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildWaitPakinItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchWaitPakinPage(params = {}) {
+ return request.post({
+ url: '/waitPakin/page',
+ params: buildWaitPakinPageParams(params)
+ })
+}
+
+export function fetchWaitPakinList() {
+ return request.post({
+ url: '/waitPakin/list',
+ data: {}
+ })
+}
+
+export function fetchWaitPakinMany(ids) {
+ return request.post({
+ url: `/waitPakin/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchWaitPakinDetail(id) {
+ return request.get({
+ url: `/waitPakin/${id}`
+ })
+}
+
+export function fetchDeleteWaitPakin(ids) {
+ return request.post({
+ url: `/waitPakin/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchWaitPakinQuery(condition = '') {
+ return request.post({
+ url: '/waitPakin/query',
+ params: { condition: normalizeText(condition) }
+ })
+}
+
+export function fetchWaitPakinItemPage(params = {}) {
+ return request.post({
+ url: '/waitPakinItem/page',
+ params: buildWaitPakinItemPageParams(params)
+ })
+}
+
+export function fetchMergeWaitPakinTasks(params = {}) {
+ return request.post({
+ url: '/waitPakin/merge',
+ params
+ })
+}
+
+export async function fetchExportWaitPakinReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/waitPakin/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/warehouse-areas-item.js b/rsf-design/src/api/warehouse-areas-item.js
new file mode 100644
index 0000000..4782284
--- /dev/null
+++ b/rsf-design/src/api/warehouse-areas-item.js
@@ -0,0 +1,80 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids.map((id) => String(id).trim()).filter(Boolean).join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+export function buildWarehouseAreasItemPageParams(params = {}) {
+ const entries = Object.entries(params).filter(([key, value]) => {
+ if (['current', 'pageSize', 'size'].includes(key)) {
+ return false
+ }
+ if (value === undefined || value === null) {
+ return false
+ }
+ if (typeof value === 'string' && value.trim() === '') {
+ return false
+ }
+ return true
+ })
+
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...Object.fromEntries(entries.map(([key, value]) => [key, normalizeText(value)]))
+ }
+}
+
+export function buildWarehouseAreasItemIsptPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.id !== undefined ? { id: params.id } : {})
+ }
+}
+
+export function fetchWarehouseAreasItemPage(params = {}) {
+ return request.post({
+ url: '/warehouseAreasItem/page',
+ params: buildWarehouseAreasItemPageParams(params)
+ })
+}
+
+export function fetchWarehouseAreasItemIsptPage(params = {}) {
+ return request.post({
+ url: '/warehouseAreasItem/ispts/page',
+ params: buildWarehouseAreasItemIsptPageParams(params)
+ })
+}
+
+export function fetchGetWarehouseAreasItemMany(ids) {
+ const normalizedIds = normalizeIds(ids)
+ return request.post({
+ url: `/warehouseAreasItem/many/${normalizedIds}`
+ })
+}
+
+export function fetchEnabledFields() {
+ return request.get({ url: '/fields/enable/list' })
+}
+
+export async function fetchExportWarehouseAreasItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/warehouseAreasItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/warehouse-stock.js b/rsf-design/src/api/warehouse-stock.js
new file mode 100644
index 0000000..edd01bd
--- /dev/null
+++ b/rsf-design/src/api/warehouse-stock.js
@@ -0,0 +1,60 @@
+import request from '@/utils/http'
+
+export function buildWarehouseStockPageParams(params = {}) {
+ const matnrCode = typeof params.matnrCode === 'string' ? params.matnrCode.trim() : params.matnrCode
+ const maktx = typeof params.maktx === 'string' ? params.maktx.trim() : params.maktx
+ const batch = typeof params.batch === 'string' ? params.batch.trim() : params.batch
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.aggType !== undefined ? { aggType: params.aggType } : {}),
+ ...(matnrCode ? { matnrCode } : {}),
+ ...(maktx ? { maktx } : {}),
+ ...(batch ? { batch } : {}),
+ ...Object.fromEntries(
+ Object.entries(params).filter(
+ ([key, value]) =>
+ !['current', 'pageSize', 'size', 'aggType', 'matnrCode', 'maktx', 'batch'].includes(key) &&
+ value !== undefined &&
+ value !== ''
+ )
+ )
+ }
+}
+
+export function buildWarehouseStockInfoParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.aggType !== undefined ? { aggType: params.aggType } : {}),
+ ...(params.stock !== undefined ? { stock: params.stock } : {})
+ }
+}
+
+export function buildWarehouseStockHistoriesParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.aggType !== undefined ? { aggType: params.aggType } : {}),
+ ...(params.stock !== undefined ? { stock: params.stock } : {})
+ }
+}
+
+export function fetchWarehouseStockPage(params = {}) {
+ return request.post({ url: '/warehouse/stock/page', params: buildWarehouseStockPageParams(params) })
+}
+
+export function fetchWarehouseStockInfoPage(params = {}) {
+ return request.post({ url: '/warehouse/stock/info', params: buildWarehouseStockInfoParams(params) })
+}
+
+export function fetchWarehouseStockHistoriesPage(params = {}) {
+ return request.post({
+ url: '/warehouse/stock/histories/page',
+ params: buildWarehouseStockHistoriesParams(params)
+ })
+}
+
+export function fetchEnabledFields() {
+ return request.get({ url: '/fields/enable/list' })
+}
diff --git a/rsf-design/src/api/warehouse.js b/rsf-design/src/api/warehouse.js
new file mode 100644
index 0000000..26a9e7d
--- /dev/null
+++ b/rsf-design/src/api/warehouse.js
@@ -0,0 +1,140 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildWarehousePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildWarehouseSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ name: normalizeText(formData.name) || '',
+ code: normalizeText(formData.code) || '',
+ factory: normalizeText(formData.factory) || '',
+ address: normalizeText(formData.address) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildWarehouseSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ factory: normalizeText(params.factory),
+ code: normalizeText(params.code),
+ name: normalizeText(params.name),
+ address: normalizeText(params.address),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildWarehouseQueryParams(condition = '') {
+ return {
+ condition: normalizeText(condition)
+ }
+}
+
+export function fetchWarehousePage(params = {}) {
+ return request.post({
+ url: '/warehouse/page',
+ params: buildWarehousePageParams(params)
+ })
+}
+
+export function fetchGetWarehouseDetail(id) {
+ return request.get({
+ url: `/warehouse/${id}`
+ })
+}
+
+export function fetchGetWarehouseMany(ids) {
+ return request.post({
+ url: `/warehouse/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchSaveWarehouse(params = {}) {
+ return request.post({
+ url: '/warehouse/save',
+ params: buildWarehouseSavePayload(params)
+ })
+}
+
+export function fetchUpdateWarehouse(params = {}) {
+ return request.post({
+ url: '/warehouse/update',
+ params: buildWarehouseSavePayload(params)
+ })
+}
+
+export function fetchDeleteWarehouse(ids) {
+ return request.post({
+ url: `/warehouse/remove/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchWarehouseQuery(condition = '') {
+ return request.post({
+ url: '/warehouse/query',
+ params: buildWarehouseQueryParams(condition)
+ })
+}
+
+export async function fetchExportWarehouseReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/warehouse/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/wave-item.js b/rsf-design/src/api/wave-item.js
new file mode 100644
index 0000000..b85fd25
--- /dev/null
+++ b/rsf-design/src/api/wave-item.js
@@ -0,0 +1,71 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildWaveItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchWaveItemPage(params = {}) {
+ return request.post({
+ url: '/waveItem/page',
+ params: buildWaveItemPageParams(params)
+ })
+}
+
+export function fetchGetWaveItemDetail(id) {
+ return request.get({
+ url: `/waveItem/${id}`
+ })
+}
+
+export function fetchGetWaveItemMany(ids) {
+ return request.post({
+ url: `/waveItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportWaveItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/waveItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/api/wave.js b/rsf-design/src/api/wave.js
new file mode 100644
index 0000000..4695632
--- /dev/null
+++ b/rsf-design/src/api/wave.js
@@ -0,0 +1,145 @@
+import request from '@/utils/http'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeIds(ids) {
+ if (Array.isArray(ids)) {
+ return ids
+ .map((id) => String(id).trim())
+ .filter(Boolean)
+ .join(',')
+ }
+
+ if (ids === null || ids === undefined) {
+ return ''
+ }
+
+ return String(ids).trim()
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function buildWavePageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildWaveItemPageParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function fetchWavePage(params = {}) {
+ return request.post({
+ url: '/wave/page',
+ params: buildWavePageParams(params)
+ })
+}
+
+export function fetchGetWaveDetail(id) {
+ return request.get({
+ url: `/wave/${id}`
+ })
+}
+
+export function fetchGetWaveMany(ids) {
+ return request.post({
+ url: `/wave/many/${normalizeIds(ids)}`
+ })
+}
+
+export function fetchWavePreviewPage(params = {}) {
+ return request.post({
+ url: '/wave/locs/preview/page',
+ params: {
+ ...filterParams(params, ['current', 'pageSize', 'size']),
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+ })
+}
+
+export function fetchPauseWave(id) {
+ return request.post({
+ url: `/wave/pause/pub/${id}`
+ })
+}
+
+export function fetchContinueWave(id) {
+ return request.post({
+ url: `/wave/continue/pub/${id}`
+ })
+}
+
+export function fetchStopWave(id) {
+ return request.post({
+ url: `/wave/stop/pub/${id}`
+ })
+}
+
+export function fetchPublicWaveTask(payload = {}) {
+ return request.post({
+ url: '/wave/public/task',
+ data: payload
+ })
+}
+
+export async function fetchExportWaveReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/wave/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
+
+export function fetchWaveItemPage(params = {}) {
+ return request.post({
+ url: '/waveItem/page',
+ params: buildWaveItemPageParams(params)
+ })
+}
+
+export function fetchGetWaveItemDetail(id) {
+ return request.get({
+ url: `/waveItem/${id}`
+ })
+}
+
+export function fetchGetWaveItemMany(ids) {
+ return request.post({
+ url: `/waveItem/many/${normalizeIds(ids)}`
+ })
+}
+
+export async function fetchExportWaveItemReport(payload = {}, options = {}) {
+ return fetch(`${import.meta.env.VITE_API_URL}/waveItem/export`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(options.headers || {})
+ },
+ body: JSON.stringify(payload)
+ })
+}
diff --git a/rsf-design/src/components/biz/list-export-print/index.vue b/rsf-design/src/components/biz/list-export-print/index.vue
index b5fee68..50e72b7 100644
--- a/rsf-design/src/components/biz/list-export-print/index.vue
+++ b/rsf-design/src/components/biz/list-export-print/index.vue
@@ -1,5 +1,5 @@
<template>
- <ElSpace wrap>
+ <ElSpace v-bind="attrs" wrap>
<ElButton :disabled="disabled" @click="handleExport">瀵煎嚭</ElButton>
<ElButton :disabled="disabled" @click="handlePrint">鎵撳嵃</ElButton>
</ElSpace>
@@ -15,7 +15,7 @@
</template>
<script setup>
- import { computed } from 'vue'
+ import { computed, useAttrs } from 'vue'
import ListPrintPreviewDialog from './list-print-preview-dialog.vue'
import {
buildListExportPayload,
@@ -24,7 +24,12 @@
buildReportStyleMeta
} from './list-export-print.helpers.js'
- defineOptions({ name: 'ListExportPrint' })
+ defineOptions({
+ name: 'ListExportPrint',
+ inheritAttrs: false
+ })
+
+ const attrs = useAttrs()
const props = defineProps({
reportTitle: { type: String, default: '鎶ヨ〃' },
diff --git a/rsf-design/src/components/core/layouts/art-breadcrumb/index.vue b/rsf-design/src/components/core/layouts/art-breadcrumb/index.vue
index 5a8c0a2..f3170a4 100644
--- a/rsf-design/src/components/core/layouts/art-breadcrumb/index.vue
+++ b/rsf-design/src/components/core/layouts/art-breadcrumb/index.vue
@@ -59,8 +59,10 @@
if (currentRouteMeta?.isIframe && (items.length === 1 || items.every(isWrapperContainer))) {
return [createBreadcrumbItem(currentRoute)]
}
- return items
+ return dedupeBreadcrumbItems(items)
})
+ const dedupeBreadcrumbItems = (items = []) =>
+ items.filter((item, index) => index === 0 || item.path !== items[index - 1]?.path)
const isWrapperContainer = (item) => item.path === '/outside' && !!item.meta?.isIframe
const createBreadcrumbItem = (route2) => ({
path: route2.path,
diff --git a/rsf-design/src/locales/langs/en.json b/rsf-design/src/locales/langs/en.json
index 3b7f747..6a5f6f8 100644
--- a/rsf-design/src/locales/langs/en.json
+++ b/rsf-design/src/locales/langs/en.json
@@ -280,6 +280,11 @@
"department": "Department",
"token": "Token",
"operation": "Operation",
+ "flowInstance": "FlowInstance",
+ "flowStepInstance": "FlowStepInstance",
+ "flowStepLog": "FlowStepLog",
+ "taskInstance": "TaskInstance",
+ "taskInstanceNode": "TaskInstanceNode",
"config": "Config",
"aiParam": "AI Params",
"aiPrompt": "Prompts",
@@ -299,6 +304,7 @@
"locArea": "locArea",
"locAreaMat": "Logic Areas",
"locAreaMatRela": "LocAreaMatRela",
+ "locAreaRela": "LocAreaRela",
"container": "Container",
"contract": "Contract",
"qlyInspect": "QlyInspect",
@@ -314,6 +320,7 @@
"asnOrderItemLog": "asnOrderItemLog",
"purchase": "Purchase",
"purchaseItem": "PurchaseItem",
+ "preparationItem": "Preparation Item",
"whMat": "Warehouse Mat",
"fields": "Extend Fields",
"fieldsItem": "Extend Fields Items",
@@ -335,6 +342,7 @@
"logs": "Logs",
"permissions": "Permissions",
"delivery": "Delivery",
+ "deliveryItem": "Delivery Item",
"outStock": "Out Stock",
"outStockItem": "Out Stock Item",
"inStockPoces": "In Stock Pocess",
@@ -343,6 +351,7 @@
"deviceBind": "Device Bind",
"tasks": "Tasks",
"wave": "Wave Manage",
+ "waveItem": "Wave Item",
"basStation": "BasStation",
"basContainer": "BasContainer",
"outBound": "Out Bound",
@@ -350,10 +359,14 @@
"stockTransfer": "Stock Transfer",
"waveRule": "Wave Rules",
"checkOrder": "Check Order",
+ "checkItem": "Check Order Item",
+ "checkDiffItem": "Check Diff Item",
"checkDiff": "Check Diff",
"transfer": "Transfer",
"transferItem": "Transfer Item",
"locRevise": "Loc Revise",
+ "reviseLog": "Loc Revise Log",
+ "reviseLogItem": "Loc Revise Log Item",
"statisticReport": "Statistical Report",
"locDeadReport": "Locs Dead Report",
"stockStatistic": "Stock Statistic",
diff --git a/rsf-design/src/locales/langs/zh.json b/rsf-design/src/locales/langs/zh.json
index fe64198..ba38b1d 100644
--- a/rsf-design/src/locales/langs/zh.json
+++ b/rsf-design/src/locales/langs/zh.json
@@ -280,6 +280,11 @@
"department": "閮ㄩ棬绠$悊",
"token": "鐧诲綍鏃ュ織",
"operation": "鎿嶄綔鏃ュ織",
+ "flowInstance": "娴佺▼瀹炰緥",
+ "flowStepInstance": "娴佺▼姝ラ瀹炰緥",
+ "flowStepLog": "娴佺▼姝ラ鏃ュ織",
+ "taskInstance": "浠诲姟瀹炰緥",
+ "taskInstanceNode": "浠诲姟瀹炰緥鑺傜偣",
"config": "閰嶇疆鍙傛暟",
"aiParam": "AI 鍙傛暟",
"aiPrompt": "Prompt 绠$悊",
@@ -299,8 +304,9 @@
"locArea": "閫昏緫鍒嗗尯(搴�)",
"locAreaMat": "閫昏緫鍒嗗尯",
"locAreaMatRela": "搴撳尯鐗╂枡鍏崇郴",
+ "locAreaRela": "搴撳尯鍏崇郴",
"container": "瀹瑰櫒绠$悊(搴�)",
- "contract": "鍚堝悓淇℃伅(搴�)",
+ "contract": "鍚堝悓淇℃伅",
"qlyInspect": "璐ㄦ淇℃伅",
"qlyIsptItem": "璐ㄦ淇℃伅鏄庣粏",
"dictType": "鏁版嵁瀛楀吀",
@@ -314,6 +320,7 @@
"asnOrderItemLog": "鏀惰揣鍘嗗彶鏄庣粏",
"purchase": "PO鍗�",
"purchaseItem": "PO鍗曟槑缁�",
+ "preparationItem": "澶囨枡鍗曟槑缁�",
"whMat": "搴撳尯鐗╂枡鍏崇郴",
"fields": "鎵╁睍瀛楁",
"fieldsItem": "鎵╁睍瀛楁鏄庣粏",
@@ -337,6 +344,7 @@
"logs": "鏃ュ織",
"permissions": "鏉冮檺绠$悊",
"delivery": "DO鍗�",
+ "deliveryItem": "DO鍗曟槑缁�",
"outStock": "鍑哄簱閫氱煡鍗�",
"outStockItem": "鍑哄簱鍗曟槑缁�",
"inStockPoces": "鍏ュ簱绠$悊",
@@ -345,6 +353,7 @@
"deviceBind": "璁惧缁戝畾",
"tasks": "浠诲姟绠$悊",
"wave": "娉㈡绠$悊",
+ "waveItem": "娉㈡鏄庣粏",
"basStation": "绔欑偣绠$悊",
"basContainer": "瀹瑰櫒瑙勫垯",
"outBound": "鍑哄簱浣滀笟",
@@ -352,10 +361,14 @@
"stockTransfer": "搴撲綅杞Щ",
"waveRule": "娉㈡绛栫暐",
"checkOrder": "鐩樼偣鍗�",
+ "checkItem": "鐩樼偣鍗曟槑缁�",
+ "checkDiffItem": "鐩樼偣宸紓鏄庣粏",
"checkDiff": "鐩樼偣宸紓鍗�",
"transfer": "璋冩嫈鍗�",
"transferItem": "璋冩嫈鍗曟槑缁�",
"locRevise": "搴撳瓨璋冩暣",
+ "reviseLog": "搴撲綅璋冩暣鏃ュ織",
+ "reviseLogItem": "搴撲綅璋冩暣鏃ュ織鏄庣粏",
"statisticReport": "鎶ヨ〃绠$悊",
"locDeadReport": "搴撳瓨鍋滄粸鎶ヨ〃",
"stockStatistic": "鏃ュ叆搴撴眹鎬绘煡璇�",
diff --git a/rsf-design/src/plugins/iconify.collections.js b/rsf-design/src/plugins/iconify.collections.js
index 632d84d..a56d4b7 100644
--- a/rsf-design/src/plugins/iconify.collections.js
+++ b/rsf-design/src/plugins/iconify.collections.js
@@ -58,11 +58,17 @@
'add-fill': {
body: '<path fill="currentColor" d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/>'
},
+ 'add-line': {
+ body: '<path fill="currentColor" d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/>'
+ },
'align-right': {
body: '<path fill="currentColor" d="M3 4h18v2H3zm4 15h14v2H7zm-4-5h18v2H3zm4-5h14v2H7z"/>'
},
'archive-line': {
body: '<path fill="currentColor" d="M3 10H2V4.003C2 3.449 2.455 3 2.992 3h18.016A.99.99 0 0 1 22 4.003V10h-1v10.002a.996.996 0 0 1-.993.998H3.993A.996.996 0 0 1 3 20.002zm16 0H5v9h14zM4 5v3h16V5zm5 7h6v2H9z"/>'
+ },
+ 'archive-stack-line': {
+ body: '<path fill="currentColor" d="M4 5h16V3H4zm16 4H4V7h16zM3 11h7v2h4v-2h7v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zm13 2v2H8v-2H5v6h14v-6z"/>'
},
'arrow-down-wide-fill': {
body: '<path fill="currentColor" d="m12 15.632l8.968-4.748l-.936-1.768L12 13.368L3.968 9.116l-.936 1.768z"/>'
@@ -109,6 +115,9 @@
'check-fill': {
body: '<path fill="currentColor" d="m10 15.17l9.192-9.191l1.414 1.414L10 17.999l-6.364-6.364l1.414-1.414z"/>'
},
+ 'check-line': {
+ body: '<path fill="currentColor" d="m10 15.17l9.192-9.191l1.414 1.414L10 17.999l-6.364-6.364l1.414-1.414z"/>'
+ },
'checkbox-blank-circle-line': {
body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16"/>'
},
@@ -124,6 +133,9 @@
'close-large-fill': {
body: '<path fill="currentColor" d="M10.586 12L2.793 4.207l1.414-1.414L12 10.586l7.793-7.793l1.414 1.414L13.414 12l7.793 7.793l-1.414 1.414L12 13.414l-7.793 7.793l-1.414-1.414z"/>'
},
+ 'coin-line': {
+ body: '<path fill="currentColor" d="M12.005 4.003c6.075 0 11 2.686 11 6v4c0 3.314-4.925 6-11 6c-5.967 0-10.824-2.591-10.995-5.823l-.005-.177v-4c0-3.314 4.925-6 11-6m0 12c-3.72 0-7.01-1.008-9-2.55v.55c0 1.882 3.883 4 9 4c5.01 0 8.838-2.03 8.995-3.882l.005-.118l.001-.55c-1.99 1.542-5.28 2.55-9.001 2.55m0-10c-5.117 0-9 2.118-9 4s3.883 4 9 4s9-2.118 9-4s-3.883-4-9-4"/>'
+ },
'command-fill': {
body: '<path fill="currentColor" d="M10 8h4V6.5a3.5 3.5 0 1 1 3.5 3.5H16v4h1.5a3.5 3.5 0 1 1-3.5 3.5V16h-4v1.5A3.5 3.5 0 1 1 6.5 14H8v-4H6.5A3.5 3.5 0 1 1 10 6.5zM8 8V6.5A1.5 1.5 0 1 0 6.5 8zm0 8H6.5A1.5 1.5 0 1 0 8 17.5zm8-8h1.5A1.5 1.5 0 1 0 16 6.5zm0 8v1.5a1.5 1.5 0 1 0 1.5-1.5zm-6-6v4h4v-4z"/>'
},
@@ -136,8 +148,14 @@
'delete-bin-5-line': {
body: '<path fill="currentColor" d="M4 8h16v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm2 2v10h12V10zm3 2h2v6H9zm4 0h2v6h-2zM7 5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v2h5v2H2V5zm2-1v1h6V4z"/>'
},
+ 'delete-bin-6-line': {
+ body: '<path fill="currentColor" d="M7 4V2h10v2h5v2h-2v15a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6H2V4zM6 6v14h12V6zm3 3h2v8H9zm4 0h2v8h-2z"/>'
+ },
'drag-move-2-fill': {
body: '<path fill="currentColor" d="M18 11V8l4 4l-4 4v-3h-5v5h3l-4 4l-4-4h3v-5H6v3l-4-4l4-4v3h5V6H8l4-4l4 4h-3v5z"/>'
+ },
+ 'drag-move-2-line': {
+ body: '<path fill="currentColor" d="M11 11V5.828L9.172 7.657L7.757 6.243L12 2l4.243 4.243l-1.415 1.414L13 5.828V11h5.172l-1.829-1.828l1.414-1.415L22 12l-4.243 4.243l-1.414-1.415L18.172 13H13v5.172l1.828-1.829l1.415 1.414L12 22l-4.243-4.243l1.415-1.414L11 18.172V13H5.828l1.829 1.828l-1.414 1.415L2 12l4.243-4.243l1.414 1.415L5.828 11z"/>'
},
'dribbble-fill': {
body: '<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2m6.605 4.61a8.5 8.5 0 0 1 1.93 5.314c-.281-.054-3.101-.629-5.943-.271c-.065-.141-.12-.293-.184-.445a25 25 0 0 0-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362M12 3.475c2.17 0 4.154.814 5.662 2.148c-.152.216-1.443 1.941-4.48 3.08c-1.399-2.57-2.95-4.675-3.189-5A8.7 8.7 0 0 1 12 3.475m-3.633.803a54 54 0 0 1 3.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.58 8.58 0 0 1 4.729-5.975M3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215c.25.477.477.965.694 1.453c-.109.033-.228.065-.336.098c-4.404 1.42-6.747 5.303-6.942 5.629a8.52 8.52 0 0 1-2.19-5.705M12 20.547a8.48 8.48 0 0 1-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337c.022-.01.033-.01.054-.022a35.3 35.3 0 0 1 1.823 6.475a8.4 8.4 0 0 1-3.341.684m4.761-1.465c-.086-.52-.542-3.015-1.66-6.084c2.68-.423 5.023.271 5.315.369a8.47 8.47 0 0 1-3.655 5.715"/>'
@@ -154,8 +172,14 @@
'eye-line': {
body: '<path fill="currentColor" d="M12 3c5.392 0 9.878 3.88 10.819 9c-.94 5.12-5.427 9-10.819 9s-9.878-3.88-10.818-9C2.122 6.88 6.608 3 12 3m0 16a9.005 9.005 0 0 0 8.778-7a9.005 9.005 0 0 0-17.555 0A9.005 9.005 0 0 0 12 19m0-2.5a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m0-2a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5"/>'
},
+ 'file-check-line': {
+ body: '<path fill="currentColor" d="M12 20v2H3.993A1 1 0 0 1 3 21.008V2.992C3 2.444 3.447 2 3.999 2H16l5 5v7h-2V8h-4V4H5v16zm2.465-.535L18 23l4.95-4.95l-1.414-1.414L18 20.172l-2.12-2.122z"/>'
+ },
'file-list-3-line': {
body: '<path fill="currentColor" d="M19 22H5a3 3 0 0 1-3-3V3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v12h4v4a3 3 0 0 1-3 3m-1-5v2a1 1 0 1 0 2 0v-2zm-2 3V4H4v15a1 1 0 0 0 1 1zM6 7h8v2H6zm0 4h8v2H6zm0 4h5v2H6z"/>'
+ },
+ 'file-warning-line': {
+ body: '<path fill="currentColor" d="M15 4H5v16h14V8h-4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008zM11 15h2v2h-2zm0-8h2v6h-2z"/>'
},
'fire-line': {
body: '<path fill="currentColor" d="M12 23a7.5 7.5 0 0 0 7.5-7.5c0-.866-.23-1.697-.5-2.47q-2.5 2.47-3.8 2.47c3.995-7 1.8-10-4.2-14c.5 5-2.796 7.274-4.138 8.537A7.5 7.5 0 0 0 12 23m.71-17.765c3.241 2.75 3.257 4.887.753 9.274c-.761 1.333.202 2.991 1.737 2.991c.688 0 1.384-.2 2.119-.595a5.5 5.5 0 1 1-9.087-5.412c.126-.118.765-.685.793-.71c.424-.38.773-.717 1.118-1.086c1.23-1.318 2.114-2.78 2.566-4.462"/>'
@@ -238,8 +262,14 @@
'notification-3-line': {
body: '<path fill="currentColor" d="M20 17h2v2H2v-2h2v-7a8 8 0 1 1 16 0zm-2 0v-7a6 6 0 0 0-12 0v7zm-9 4h6v2H9z"/>'
},
+ 'paint-line': {
+ body: '<path fill="currentColor" d="m19.228 18.732l1.767-1.767l1.768 1.767a2.5 2.5 0 1 1-3.535 0M8.878 1.08l11.314 11.313a1 1 0 0 1 0 1.415l-8.485 8.485a1 1 0 0 1-1.414 0l-8.485-8.485a1 1 0 0 1 0-1.415l7.778-7.778l-2.122-2.121zM11 6.03L3.929 13.1l7.07 7.072l7.072-7.071z"/>'
+ },
'palette-line': {
body: '<path fill="currentColor" d="M12 2c5.522 0 10 3.978 10 8.889a5.56 5.56 0 0 1-5.556 5.555h-1.966c-.922 0-1.667.745-1.667 1.667c0 .422.167.811.422 1.1c.267.3.434.689.434 1.122C13.667 21.256 12.9 22 12 22C6.478 22 2 17.522 2 12S6.478 2 12 2m-1.189 16.111a3.664 3.664 0 0 1 3.667-3.667h1.966A3.56 3.56 0 0 0 20 10.89C20 7.139 16.468 4 12 4a8 8 0 0 0-.676 15.972a3.65 3.65 0 0 1-.513-1.86M7.5 12a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m9 0a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3M12 9a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3"/>'
+ },
+ 'pause-circle-line': {
+ body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16M9 9h2v6H9zm4 0h2v6h-2z"/>'
},
'pencil-line': {
body: '<path fill="currentColor" d="m15.728 9.576l-1.414-1.414L5 17.476v1.414h1.414zm1.414-1.414l1.414-1.414l-1.414-1.414l-1.414 1.414zm-9.9 12.728H3v-4.243L16.435 3.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414z"/>'
@@ -247,17 +277,35 @@
'pie-chart-line': {
body: '<path fill="currentColor" d="M9 2.458v2.124A8.003 8.003 0 0 0 12 20a8 8 0 0 0 7.419-5h2.123c-1.274 4.057-5.064 7-9.542 7c-5.523 0-10-4.477-10-10c0-4.478 2.943-8.268 7-9.542M12 2c5.523 0 10 4.477 10 10q0 .507-.05 1H11V2.05Q11.493 2 12 2m1 2.062V11h6.938A8.004 8.004 0 0 0 13 4.062"/>'
},
+ 'play-circle-line': {
+ body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16M10.622 8.415l4.879 3.252a.4.4 0 0 1 0 .666l-4.88 3.252a.4.4 0 0 1-.621-.332V8.747a.4.4 0 0 1 .622-.332"/>'
+ },
+ 'play-line': {
+ body: '<path fill="currentColor" d="M16.394 12L10 7.737v8.526zm2.982.416L8.777 19.482A.5.5 0 0 1 8 19.066V4.934a.5.5 0 0 1 .777-.416l10.599 7.066a.5.5 0 0 1 0 .832"/>'
+ },
+ 'play-list-add-line': {
+ body: '<path fill="currentColor" d="M2 18h10v2H2zm0-7h20v2H2zm0-7h20v2H2zm16 14v-3h2v3h3v2h-3v3h-2v-3h-3v-2z"/>'
+ },
'plug-2-line': {
body: '<path fill="currentColor" d="M13 18v2h6v2h-6a2 2 0 0 1-2-2v-2H8a4 4 0 0 1-4-4V7a1 1 0 0 1 1-1h2V2h2v4h6V2h2v4h2a1 1 0 0 1 1 1v7a4 4 0 0 1-4 4zm-5-2h8a2 2 0 0 0 2-2v-3H6v3a2 2 0 0 0 2 2m10-8H6v1h12zm-6 6.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2M11 2h2v3h-2z"/>'
},
'price-tag-line': {
body: '<path fill="currentColor" d="m3.005 7l8.445-5.63a1 1 0 0 1 1.11 0L21.005 7v14a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1zm2 1.07V20h14V8.07l-7-4.667zm7 2.93a2 2 0 1 1 0-4a2 2 0 0 1 0 4"/>'
},
+ 'printer-line': {
+ body: '<path fill="currentColor" d="M17 2a1 1 0 0 1 1 1v4h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-3v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2H3a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3V3a1 1 0 0 1 1-1zm-1 15H8v3h8zm4-8H4v8h2v-1a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1h2zM8 10v2H5v-2zm8-6H8v3h8z"/>'
+ },
'progress-2-line': {
body: '<path fill="currentColor" d="M2 12c0 5.523 4.477 10 10 10s10-4.477 10-10S17.523 2 12 2S2 6.477 2 12m18 0a8 8 0 1 1-16 0a8 8 0 0 1 16 0m-8 0V6a6 6 0 0 1 6 6z"/>'
},
+ 'pulse-line': {
+ body: '<path fill="currentColor" d="m9 7.539l6 14L18.66 13H23v-2h-5.66L15 16.461l-6-14L5.34 11H1v2h5.66z"/>'
+ },
'pushpin-2-line': {
body: '<path fill="currentColor" d="M18 3v2h-1v6l2 3v2h-6v7h-2v-7H5v-2l2-3V5H6V3zM9 5v6.606L7.404 14h9.192L15 11.606V5z"/>'
+ },
+ 'pushpin-line': {
+ body: '<path fill="currentColor" d="m13.827 1.69l8.486 8.485l-1.415 1.414l-.707-.707l-4.242 4.243l-.707 3.536l-1.415 1.414l-4.242-4.243l-4.95 4.95l-1.414-1.414l4.95-4.95l-4.243-4.243l1.414-1.414l3.536-.707l4.242-4.243l-.707-.707zm.707 3.536l-4.67 4.67l-2.822.565l6.5 6.5l.564-2.822l4.671-4.67z"/>'
},
'refresh-line': {
body: '<path fill="currentColor" d="M5.463 4.433A9.96 9.96 0 0 1 12 2c5.523 0 10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3A8 8 0 0 0 6.46 6.228zm13.074 15.134A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12c0-2.136.67-4.116 1.81-5.74L7 12H4a8 8 0 0 0 13.54 5.772z"/>'
@@ -271,8 +319,14 @@
'search-line': {
body: '<path fill="currentColor" d="m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z"/>'
},
+ 'send-plane-line': {
+ body: '<path fill="currentColor" d="m21.727 2.957l-5.454 19.086c-.15.529-.475.553-.717.07L11 13L1.923 9.37c-.51-.205-.503-.51.034-.689L21.043 2.32c.529-.176.832.12.684.638m-2.692 2.14L6.812 9.17l5.637 2.255l3.04 6.08z"/>'
+ },
'server-line': {
body: '<path fill="currentColor" d="M5 11h14V5H5zm16-7v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1m-2 9H5v6h14zM7 15h3v2H7zm0-8h3v2H7z"/>'
+ },
+ 'service-line': {
+ body: '<path fill="currentColor" d="M3.161 4.469a6.5 6.5 0 0 1 8.84-.328a6.5 6.5 0 0 1 9.178 9.154l-7.765 7.79a2 2 0 0 1-2.719.102l-.11-.101l-7.764-7.791a6.5 6.5 0 0 1 .34-8.826m1.414 1.414a4.5 4.5 0 0 0-.146 6.21l.146.154L12 19.672l5.303-5.305l-3.535-3.534l-1.06 1.06a3 3 0 0 1-4.244-4.242l2.102-2.103a4.5 4.5 0 0 0-5.837.189zm8.486 2.828a1 1 0 0 1 1.414 0l4.242 4.242l.708-.706a4.5 4.5 0 0 0-6.211-6.51l-.153.146l-3.182 3.182a1 1 0 0 0-.078 1.327l.078.087a1 1 0 0 0 1.327.078l.087-.078z"/>'
},
'settings-line': {
body: '<path fill="currentColor" d="m12 1l9.5 5.5v11L12 23l-9.5-5.5v-11zm0 2.311L4.5 7.653v8.694l7.5 4.342l7.5-4.342V7.653zM12 16a4 4 0 1 1 0-8a4 4 0 0 1 0 8m0-2a2 2 0 1 0 0-4a2 2 0 0 0 0 4"/>'
@@ -283,6 +337,12 @@
'smartphone-line': {
body: '<path fill="currentColor" d="M7 4v16h10V4zM6 2h12a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1m6 15a1 1 0 1 1 0 2a1 1 0 0 1 0-2"/>'
},
+ 'snowy-line': {
+ body: '<path fill="currentColor" d="m13 16.268l1.964-1.134l1 1.732L14 18l1.964 1.134l-1 1.732L13 19.732V22h-2v-2.268l-1.964 1.134l-1-1.732L10 18l-1.964-1.134l1-1.732L11 16.268V14h2zM17 18v-2h.5a3.5 3.5 0 1 0-2.5-5.95V10a6 6 0 1 0-8 5.659v2.089a8 8 0 1 1 9.458-10.65A5.5 5.5 0 1 1 17.5 18z"/>'
+ },
+ 'stop-circle-line': {
+ body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16M9 9h6v6H9z"/>'
+ },
'store-2-line': {
body: '<path fill="currentColor" d="M21 13.242V20h1v2H2v-2h1v-6.758A4.5 4.5 0 0 1 1 9.5c0-.827.224-1.624.633-2.303L4.345 2.5a1 1 0 0 1 .866-.5H18.79a1 1 0 0 1 .866.5l2.703 4.682c.418.694.642 1.49.642 2.318c0 1.56-.794 2.935-2 3.742m-2 .73a4.5 4.5 0 0 1-3.75-1.36A4.5 4.5 0 0 1 12 14.001a4.5 4.5 0 0 1-3.25-1.387A4.5 4.5 0 0 1 5 13.973V20h14zM5.789 4L3.356 8.213a2.5 2.5 0 1 0 4.466 2.216c.335-.837 1.52-.837 1.856 0a2.5 2.5 0 0 0 4.644 0c.335-.837 1.52-.837 1.856 0a2.5 2.5 0 1 0 4.457-2.232L18.21 4z"/>'
},
@@ -292,6 +352,12 @@
'table-line': {
body: '<path fill="currentColor" d="M4 8h16V5H4zm10 11v-9h-4v9zm2 0h4v-9h-4zm-8 0v-9H4v9zM3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1"/>'
},
+ 'task-line': {
+ body: '<path fill="currentColor" d="M19 4H5v16h14zM3 2.992C3 2.444 3.447 2 3.999 2h16a1 1 0 0 1 1 1L21 20.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008zm8.293 10.13l4.243-4.243l1.414 1.414l-5.657 5.657l-3.89-3.89l1.415-1.414z"/>'
+ },
+ 'timer-flash-line': {
+ body: '<path fill="currentColor" d="M6.382 5.968A8.96 8.96 0 0 1 12 4c2.125 0 4.078.736 5.618 1.968l1.453-1.453l1.414 1.414l-1.453 1.453A9 9 0 1 1 3 13c0-2.125.736-4.078 1.968-5.618L3.515 5.93l1.414-1.414zM12 20a7 7 0 1 0 0-14a7 7 0 0 0 0 14m1-8h3l-5 6.5V14H8l5-6.505zM8 1h8v2H8z"/>'
+ },
'tools-line': {
body: '<path fill="currentColor" d="M5.33 3.272a3.5 3.5 0 0 1 4.254 4.962l10.709 10.71l-1.414 1.414l-10.71-10.71a3.502 3.502 0 0 1-4.962-4.255L5.444 7.63a1.5 1.5 0 0 0 2.121-2.121zm10.367 1.883l3.182-1.768l1.414 1.415l-1.768 3.182l-1.768.353l-2.12 2.121l-1.415-1.414l2.121-2.121zm-6.718 8.132l1.415 1.414l-5.304 5.303a1 1 0 0 1-1.492-1.327l.078-.087z"/>'
},
diff --git a/rsf-design/src/router/adapters/backendMenuAdapter.js b/rsf-design/src/router/adapters/backendMenuAdapter.js
index cabcb5d..fae5b8a 100644
--- a/rsf-design/src/router/adapters/backendMenuAdapter.js
+++ b/rsf-design/src/router/adapters/backendMenuAdapter.js
@@ -20,22 +20,85 @@
whMat: '/basic-info/wh-mat',
matnr: '/basic-info/wh-mat',
matnrGroup: '/basic-info/matnr-group',
+ locType: '/basic-info/loc-type',
+ taskPathTemplate: '/basic-info/task-path-template',
+ taskPathTemplateMerge: '/basic-info/task-path-template-merge',
+ taskPathTemplateNode: '/basic-info/task-path-template-node',
basContainer: '/basic-info/bas-container',
+ container: '/basic-info/bas-container',
+ contract: '/basic-info/contract',
+ basStation: '/basic-info/bas-station',
+ basStationArea: '/basic-info/bas-station-area',
+ deviceSite: '/basic-info/device-site',
+ deviceBind: '/basic-info/device-bind',
+ companys: '/basic-info/companys',
warehouse: '/basic-info/warehouse',
warehouseAreas: '/basic-info/warehouse-areas',
loc: '/basic-info/loc',
+ locArea: '/basic-info/loc-area',
+ locAreaMat: '/basic-info/loc-area-mat',
+ locAreaMatRela: '/basic-info/loc-area-mat-rela',
+ locAreaRela: '/basic-info/loc-area-rela',
+ asnOrder: '/orders/asn-order',
+ asnOrderItem: '/orders/asn-order-item',
+ asnOrderLog: '/orders/asn-order-log',
+ purchase: '/orders/purchase',
+ purchaseItem: '/orders/purchase-item',
+ qlyIsptItem: '/manager/qly-ispt-item',
+ waitPakin: '/orders/wait-pakin',
+ waitPakinItem: '/orders/wait-pakin-item',
+ waitPakinLog: '/orders/wait-pakin-log',
+ waitPakinItemLog: '/orders/wait-pakin-item-log',
+ delivery: '/orders/delivery',
+ deliveryItem: '/orders/delivery-item',
+ outStock: '/orders/out-stock',
+ outStockItem: '/orders/out-stock-item',
+ check: '/orders/check',
+ checkItem: '/orders/check-item',
+ checkDiff: '/orders/check-diff',
+ checkDiffItem: '/orders/check-diff-item',
+ preparation: '/orders/preparation',
+ abnormal: '/abnormal',
warehouseStock: '/stock/warehouse-stock',
+ locItem: '/manager/loc-item',
warehouseAreasItem: '/stock/warehouse-areas-item',
qlyInspect: '/manager/qly-inspect',
locRevise: '/manager/loc-revise',
+ reviseLog: '/manager/revise-log',
+ reviseLogItem: '/manager/revise-log-item',
freeze: '/manager/freeze',
stock: '/manager/stock',
task: '/manager/task',
+ taskItem: '/manager/task-item',
+ taskLog: '/manager/task-log',
+ taskItemLog: '/manager/task-item-log',
locPreview: '/manager/loc-preview',
+ locDeadReport: '/manager/loc-dead-report',
+ checkOutBound: '/work/check-out-bound',
+ outBound: '/work/out-bound',
+ wave: '/orders/wave',
+ waveItem: '/orders/wave-item',
+ transfer: '/orders/transfer',
+ transferItem: '/orders/transfer-item',
+ statisticCount: '/reports/statistic-count',
+ inStatistic: '/statistics/in-statistic',
+ inStatisticItem: '/statistics/in-statistic-item',
+ outStatistic: '/statistics/out-statistic',
+ outStatisticItem: '/statistics/out-statistic-item',
+ stockTransfer: '/stock/stock-transfer',
waveRule: '/manager/wave-rule',
menuPda: '/manager/menu-pda',
+ stockItem: '/manager/stock-item',
serialRule: '/system/serial-rule',
+ serialRuleItem: '/system/serial-rule-item',
operationRecord: '/system/operation-record',
+ flowInstance: '/system/flow-instance',
+ flowStepInstance: '/system/flow-step-instance',
+ flowStepTemplate: '/system/flow-step-template',
+ subsystemFlowTemplate: '/system/subsystem-flow-template',
+ flowStepLog: '/system/flow-step-log',
+ taskInstance: '/system/task-instance',
+ taskInstanceNode: '/system/task-instance-node',
userLogin: '/system/user-login'
}
diff --git a/rsf-design/src/router/core/RouteRegistry.js b/rsf-design/src/router/core/RouteRegistry.js
index bccc392..3132e78 100644
--- a/rsf-design/src/router/core/RouteRegistry.js
+++ b/rsf-design/src/router/core/RouteRegistry.js
@@ -1,10 +1,10 @@
-import { ComponentLoader } from './ComponentLoader'
-import { RouteValidator } from './RouteValidator'
-import { RouteTransformer } from './RouteTransformer'
+import { ComponentLoader } from './ComponentLoader.js'
+import { RouteValidator } from './RouteValidator.js'
+import { RouteTransformer } from './RouteTransformer.js'
class RouteRegistry {
- constructor(router) {
+ constructor(router, options = {}) {
this.router = router
- this.componentLoader = new ComponentLoader()
+ this.componentLoader = options.componentLoader || new ComponentLoader()
this.validator = new RouteValidator()
this.transformer = new RouteTransformer(this.componentLoader)
this.removeRouteFns = []
@@ -23,11 +23,27 @@
throw new Error(`璺敱閰嶇疆楠岃瘉澶辫触: ${validationResult.errors.join(', ')}`)
}
const removeRouteFns = []
+ const existingRoutePaths = new Set(
+ (this.router.getRoutes?.() || []).map((route) => route?.path).filter(Boolean)
+ )
menuList.forEach((route) => {
- if (route.name && !this.router.hasRoute(route.name)) {
- const routeConfig = this.transformer.transform(route)
- const removeRouteFn = this.router.addRoute(routeConfig)
- removeRouteFns.push(removeRouteFn)
+ const routeConfig = this.transformer.transform(route)
+ const routePath = routeConfig?.path
+
+ if (routePath && existingRoutePaths.has(routePath)) {
+ console.warn(`[RouteRegistry] 妫�娴嬪埌閲嶅璺緞锛屽凡璺宠繃鍔ㄦ�佹敞鍐�: ${routePath}`)
+ return
+ }
+
+ if (route.name && this.router.hasRoute(route.name)) {
+ console.warn(`[RouteRegistry] 妫�娴嬪埌閲嶅鍚嶇О锛屽凡璺宠繃鍔ㄦ�佹敞鍐�: ${route.name}`)
+ return
+ }
+
+ const removeRouteFn = this.router.addRoute(routeConfig)
+ removeRouteFns.push(removeRouteFn)
+ if (routePath) {
+ existingRoutePaths.add(routePath)
}
})
this.removeRouteFns = removeRouteFns
diff --git a/rsf-design/src/router/core/RouteTransformer.js b/rsf-design/src/router/core/RouteTransformer.js
index 37456f8..a653a1d 100644
--- a/rsf-design/src/router/core/RouteTransformer.js
+++ b/rsf-design/src/router/core/RouteTransformer.js
@@ -1,4 +1,4 @@
-import { IframeRouteManager } from './IframeRouteManager'
+import { IframeRouteManager } from './IframeRouteManager.js'
class RouteTransformer {
constructor(componentLoader) {
this.componentLoader = componentLoader
diff --git a/rsf-design/src/router/core/RouteValidator.js b/rsf-design/src/router/core/RouteValidator.js
index 1be4918..044887c 100644
--- a/rsf-design/src/router/core/RouteValidator.js
+++ b/rsf-design/src/router/core/RouteValidator.js
@@ -1,4 +1,4 @@
-import { RoutesAlias } from '../routesAlias'
+import { RoutesAlias } from '../routesAlias.js'
class RouteValidator {
constructor() {
this.warnedRoutes = new Set()
@@ -23,11 +23,19 @@
*/
checkDuplicates(routes, errors, warnings, parentPath = '') {
const routeNameMap = /* @__PURE__ */ new Map()
+ const routePathMap = /* @__PURE__ */ new Map()
const componentPathMap = /* @__PURE__ */ new Map()
const checkRoutes = (routes2, parentPath2 = '') => {
routes2.forEach((route) => {
const currentPath = route.path || ''
const fullPath = this.resolvePath(parentPath2, currentPath)
+ if (fullPath) {
+ if (routePathMap.has(fullPath)) {
+ warnings.push(`璺敱璺緞閲嶅: "${fullPath}"`)
+ } else {
+ routePathMap.set(fullPath, String(route.name || fullPath))
+ }
+ }
if (route.name) {
const routeName = String(route.name)
if (routeNameMap.has(routeName)) {
diff --git a/rsf-design/src/router/guards/beforeEach.js b/rsf-design/src/router/guards/beforeEach.js
index d49d94a..8a71148 100644
--- a/rsf-design/src/router/guards/beforeEach.js
+++ b/rsf-design/src/router/guards/beforeEach.js
@@ -99,20 +99,41 @@
})
return false
}
+function normalizeRoutePathSegment(path = '') {
+ if (!path) {
+ return ''
+ }
+ return path.replace(/^\/+/, '').replace(/\/+$/, '')
+}
+
+function joinRoutePath(parentPath = '', childPath = '') {
+ if (!childPath) {
+ return parentPath || '/'
+ }
+ if (childPath.startsWith('/')) {
+ return childPath
+ }
+
+ const normalizedParent = parentPath === '/' ? '' : normalizeRoutePathSegment(parentPath)
+ const normalizedChild = normalizeRoutePathSegment(childPath)
+ const fullPath = [normalizedParent, normalizedChild].filter(Boolean).join('/')
+ return `/${fullPath}`.replace(/\/+/g, '/')
+}
+
function isStaticRoute(path) {
- const checkRoute = (routes, targetPath) => {
+ const checkRoute = (routes, targetPath, parentPath = '') => {
return routes.some((route) => {
if (route.name === 'Exception404') {
return false
}
- const routePath = route.path
+ const routePath = joinRoutePath(parentPath, route.path)
const pattern = routePath.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*')
const regex = new RegExp(`^${pattern}$`)
if (regex.test(targetPath)) {
return true
}
if (route.children && route.children.length > 0) {
- return checkRoute(route.children, targetPath)
+ return checkRoute(route.children, targetPath, routePath)
}
return false
})
diff --git a/rsf-design/src/router/index.js b/rsf-design/src/router/index.js
index 5e2da45..7f51332 100644
--- a/rsf-design/src/router/index.js
+++ b/rsf-design/src/router/index.js
@@ -14,5 +14,5 @@
setupAfterEachGuard(router)
app.use(router)
}
-const HOME_PAGE_PATH = ''
+const HOME_PAGE_PATH = '/dashboard/console'
export { HOME_PAGE_PATH, initRouter, router }
diff --git a/rsf-design/src/router/routes/staticRoutes.js b/rsf-design/src/router/routes/staticRoutes.js
index 13cebd2..7dafda8 100644
--- a/rsf-design/src/router/routes/staticRoutes.js
+++ b/rsf-design/src/router/routes/staticRoutes.js
@@ -1,11 +1,4 @@
const staticRoutes = [
- // 涓嶉渶瑕佺櫥褰曞氨鑳借闂殑璺敱绀轰緥
- // {
- // path: '/welcome',
- // name: 'WelcomeStatic',
- // component: () => import('@views/dashboard/console/index.vue'),
- // meta: { title: 'menus.dashboard.title' }
- // },
{
path: '/dashboard',
component: () => import('@views/index/index.vue'),
@@ -21,74 +14,6 @@
icon: 'ri:home-smile-2-line',
keepAlive: false,
fixedTab: true
- }
- }
- ]
- },
- {
- path: '/basic-info',
- component: () => import('@views/index/index.vue'),
- name: 'BasicInfo',
- meta: { title: 'menu.basicInfo' },
- children: [
- {
- path: 'wh-mat',
- name: 'WhMat',
- component: () => import('@views/basic-info/wh-mat/index.vue'),
- meta: {
- title: 'menu.matnr',
- icon: 'ri:bill-line',
- keepAlive: false
- }
- },
- {
- path: 'matnr-group',
- name: 'MatnrGroup',
- component: () => import('@views/basic-info/matnr-group/index.vue'),
- meta: {
- title: 'menu.matnrGroup',
- icon: 'ri:git-branch-line',
- keepAlive: false
- }
- },
- {
- path: 'bas-container',
- name: 'BasContainer',
- component: () => import('@views/basic-info/bas-container/index.vue'),
- meta: {
- title: 'menu.basContainer',
- icon: 'ri:archive-line',
- keepAlive: false
- }
- },
- {
- path: 'warehouse',
- name: 'Warehouse',
- component: () => import('@views/basic-info/warehouse/index.vue'),
- meta: {
- title: 'menu.warehouse',
- icon: 'ri:store-2-line',
- keepAlive: false
- }
- },
- {
- path: 'warehouse-areas',
- name: 'WarehouseAreas',
- component: () => import('@views/basic-info/warehouse-areas/index.vue'),
- meta: {
- title: 'menu.warehouseAreas',
- icon: 'ri:layout-grid-line',
- keepAlive: false
- }
- },
- {
- path: 'loc',
- name: 'Loc',
- component: () => import('@views/basic-info/loc/index.vue'),
- meta: {
- title: 'menu.loc',
- icon: 'ri:map-pin-2-line',
- keepAlive: false
}
}
]
@@ -118,12 +43,6 @@
meta: { title: '403', isHideTab: true }
},
{
- path: '/:pathMatch(.*)*',
- name: 'Exception404',
- component: () => import('@views/exception/404/index.vue'),
- meta: { title: '404', isHideTab: true }
- },
- {
path: '/500',
name: 'Exception500',
component: () => import('@views/exception/500/index.vue'),
@@ -135,7 +54,6 @@
name: 'Outside',
meta: { title: 'menus.outside.title' },
children: [
- // iframe 鍐呭祵椤甸潰
{
path: '/outside/iframe/:path',
name: 'Iframe',
@@ -143,6 +61,13 @@
meta: { title: 'iframe' }
}
]
+ },
+ {
+ path: '/:pathMatch(.*)*',
+ name: 'Exception404',
+ component: () => import('@views/exception/404/index.vue'),
+ meta: { title: '404', isHideTab: true }
}
]
+
export { staticRoutes }
diff --git a/rsf-design/src/store/modules/menu.js b/rsf-design/src/store/modules/menu.js
index db546d2..f7dded2 100644
--- a/rsf-design/src/store/modules/menu.js
+++ b/rsf-design/src/store/modules/menu.js
@@ -1,6 +1,5 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
-import { getFirstMenuPath } from '@/utils'
import { HOME_PAGE_PATH } from '@/router'
const useMenuStore = defineStore('menuStore', () => {
const homePath = ref(HOME_PAGE_PATH)
@@ -9,7 +8,7 @@
const removeRouteFns = ref([])
const setMenuList = (list) => {
menuList.value = list
- setHomePath(HOME_PAGE_PATH || getFirstMenuPath(list))
+ setHomePath(HOME_PAGE_PATH)
}
const getHomePath = () => homePath.value
const setHomePath = (path) => {
diff --git a/rsf-design/src/store/modules/worktab.js b/rsf-design/src/store/modules/worktab.js
index da66bf0..b06c68b 100644
--- a/rsf-design/src/store/modules/worktab.js
+++ b/rsf-design/src/store/modules/worktab.js
@@ -4,6 +4,14 @@
import { useCommon } from '@/hooks/core/useCommon'
import { normalizeIcon } from '@/router/adapters/backendMenuAdapter.js'
+function normalizeLegacyTabPath(path) {
+ if (path === '/dashboard') {
+ return '/dashboard/console'
+ }
+
+ return path
+}
+
const normalizeTabState = (tab) => {
if (!tab || typeof tab !== 'object') {
return tab
@@ -11,6 +19,7 @@
return {
...tab,
+ path: normalizeLegacyTabPath(tab.path),
icon: normalizeIcon(tab.icon)
}
}
@@ -24,25 +33,30 @@
const hasOpenedTabs = computed(() => opened.value.length > 0)
const hasMultipleTabs = computed(() => opened.value.length > 1)
const currentTabIndex = computed(() =>
- current.value.path ? opened.value.findIndex((tab) => tab.path === current.value.path) : -1
+ current.value.path
+ ? opened.value.findIndex((tab) => tab.path === normalizeLegacyTabPath(current.value.path))
+ : -1
)
const findTabIndex = (path) => {
- return opened.value.findIndex((tab) => tab.path === path)
+ const resolvedPath = normalizeLegacyTabPath(path)
+ return opened.value.findIndex((tab) => tab.path === resolvedPath)
}
const getTab = (path) => {
- return opened.value.find((tab) => tab.path === path)
+ const resolvedPath = normalizeLegacyTabPath(path)
+ return opened.value.find((tab) => tab.path === resolvedPath)
}
const isTabClosable = (tab) => {
return !tab.fixedTab
}
const safeRouterPush = (tab) => {
- if (!tab.path) {
+ const resolvedPath = normalizeLegacyTabPath(tab.path)
+ if (!resolvedPath) {
console.warn('灏濊瘯璺宠浆鍒版棤鏁堣矾寰勭殑鏍囩椤�')
return
}
try {
router.push({
- path: tab.path,
+ path: resolvedPath,
query: tab.query
})
} catch (error) {
@@ -50,7 +64,8 @@
}
}
const openTab = (tab) => {
- if (!tab.path) {
+ const resolvedPath = normalizeLegacyTabPath(tab.path)
+ if (!resolvedPath) {
console.warn('灏濊瘯鎵撳紑鏃犳晥鐨勬爣绛鹃〉')
return
}
@@ -62,11 +77,14 @@
existingIndex = opened.value.findIndex((t) => t.name === tab.name)
}
if (existingIndex === -1) {
- existingIndex = findTabIndex(tab.path)
+ existingIndex = findTabIndex(resolvedPath)
}
if (existingIndex === -1) {
const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length
- const newTab = normalizeTabState(tab)
+ const newTab = normalizeTabState({
+ ...tab,
+ path: resolvedPath
+ })
if (tab.fixedTab) {
opened.value.splice(insertIndex, 0, newTab)
} else {
@@ -77,7 +95,7 @@
const existingTab = opened.value[existingIndex]
opened.value[existingIndex] = normalizeTabState({
...existingTab,
- path: tab.path,
+ path: resolvedPath,
params: tab.params,
query: tab.query,
title: tab.title || existingTab.title,
@@ -259,8 +277,9 @@
if (routes.some((r) => r.name === tab.name)) return true
}
if (tab.path) {
+ const resolvedPath = normalizeLegacyTabPath(tab.path)
const resolved = routerInstance.resolve({
- path: tab.path,
+ path: resolvedPath,
query: tab.query || void 0
})
return resolved.matched.length > 0
diff --git a/rsf-design/src/utils/backend-menu-title.js b/rsf-design/src/utils/backend-menu-title.js
index 4599d84..8db1f59 100644
--- a/rsf-design/src/utils/backend-menu-title.js
+++ b/rsf-design/src/utils/backend-menu-title.js
@@ -50,6 +50,8 @@
'menu.locItem': '搴撳瓨鏄庣粏',
'menu.locPreview': '搴撲綅鏄庣粏',
'menu.locRevise': '搴撳瓨璋冩暣',
+ 'menu.reviseLog': '搴撲綅璋冩暣鏃ュ織',
+ 'menu.reviseLogItem': '搴撲綅璋冩暣鏃ュ織鏄庣粏',
'menu.locType': '搴撲綅绫诲瀷(搴�)',
'menu.logs': '鏃ュ織',
'menu.matnr': '鐗╂枡',
diff --git a/rsf-design/src/utils/sys/requestGuard.js b/rsf-design/src/utils/sys/requestGuard.js
new file mode 100644
index 0000000..c98bc04
--- /dev/null
+++ b/rsf-design/src/utils/sys/requestGuard.js
@@ -0,0 +1,66 @@
+import { ElMessage } from 'element-plus'
+
+const DEFAULT_REQUEST_GUARD_TIMEOUT = 8e3
+const DEFAULT_REQUEST_TIMEOUT_MESSAGE = '璇锋眰瓒呮椂锛屽凡鍋滄绛夊緟'
+
+function withRequestGuard(task, fallbackValue, options = {}) {
+ const { timeoutMs = DEFAULT_REQUEST_GUARD_TIMEOUT, onFallback } = options
+
+ return new Promise((resolve) => {
+ let settled = false
+
+ const finish = (value, reason, error) => {
+ if (settled) return
+ settled = true
+ clearTimeout(timer)
+ if (typeof onFallback === 'function' && reason) {
+ onFallback(reason, error)
+ }
+ resolve(value)
+ }
+
+ const timer = setTimeout(() => finish(fallbackValue, 'timeout'), timeoutMs)
+
+ Promise.resolve(task).then(
+ (value) => finish(value),
+ (error) => finish(fallbackValue, 'error', error)
+ )
+ })
+}
+
+function guardRequestWithMessage(task, fallbackValue, options = {}) {
+ const {
+ timeoutMs = DEFAULT_REQUEST_GUARD_TIMEOUT,
+ timeoutMessage = DEFAULT_REQUEST_TIMEOUT_MESSAGE,
+ errorMessage,
+ resolveErrorMessage
+ } = options
+
+ return withRequestGuard(task, fallbackValue, {
+ timeoutMs,
+ onFallback: (reason, error) => {
+ if (reason === 'timeout') {
+ if (timeoutMessage) {
+ ElMessage.warning(timeoutMessage)
+ }
+ return
+ }
+
+ const resolvedMessage =
+ typeof resolveErrorMessage === 'function'
+ ? resolveErrorMessage(error)
+ : errorMessage || error?.message
+
+ if (resolvedMessage) {
+ ElMessage.error(resolvedMessage)
+ }
+ }
+ })
+}
+
+export {
+ DEFAULT_REQUEST_GUARD_TIMEOUT,
+ DEFAULT_REQUEST_TIMEOUT_MESSAGE,
+ guardRequestWithMessage,
+ withRequestGuard
+}
diff --git a/rsf-design/src/views/abnormal/abnormalPage.helpers.js b/rsf-design/src/views/abnormal/abnormalPage.helpers.js
new file mode 100644
index 0000000..115e753
--- /dev/null
+++ b/rsf-design/src/views/abnormal/abnormalPage.helpers.js
@@ -0,0 +1,174 @@
+import { defaultResponseAdapter } from '../../utils/table/tableUtils.js'
+
+const ABNORMAL_PAGE_TITLE = '寮傚父绠$悊'
+
+const ABNORMAL_CARD_DEFINITIONS = [
+ {
+ key: 'checkDiff',
+ title: '鐩樼偣宸紓',
+ route: '/manager/checkDiff',
+ icon: 'ri:file-warning-line',
+ iconBoxClass: 'bg-[rgba(245,108,108,0.12)]',
+ iconClass: 'text-[var(--el-color-danger)]',
+ emptyHint: '鏆傛棤宸紓鍗�'
+ },
+ {
+ key: 'qlyInspect',
+ title: '璐ㄦ璁板綍',
+ route: '/manager/qlyInspect',
+ icon: 'ri:shield-check-line',
+ iconBoxClass: 'bg-[rgba(64,158,255,0.12)]',
+ iconClass: 'text-[var(--el-color-primary)]',
+ emptyHint: '鏆傛棤璐ㄦ璁板綍'
+ },
+ {
+ key: 'freeze',
+ title: '鍐荤粨搴撳瓨',
+ route: '/manager/freeze',
+ icon: 'ri:snowy-line',
+ iconBoxClass: 'bg-[rgba(103,194,58,0.12)]',
+ iconClass: 'text-[var(--el-color-success)]',
+ emptyHint: '鏆傛棤鍐荤粨搴撳瓨'
+ },
+ {
+ key: 'locRevise',
+ title: '搴撲綅璋冩暣',
+ route: '/manager/locRevise',
+ icon: 'ri:drag-move-2-line',
+ iconBoxClass: 'bg-[rgba(230,162,60,0.12)]',
+ iconClass: 'text-[var(--el-color-warning)]',
+ emptyHint: '鏆傛棤搴撲綅璋冩暣'
+ }
+]
+
+function normalizeSummaryResponse(response) {
+ const normalized = defaultResponseAdapter(response)
+ return {
+ total: Number(normalized.total || 0),
+ records: Array.isArray(normalized.records) ? normalized.records : []
+ }
+}
+
+function createAbnormalOverviewState() {
+ return Object.fromEntries(
+ ABNORMAL_CARD_DEFINITIONS.map((item) => [
+ item.key,
+ {
+ total: 0,
+ records: []
+ }
+ ])
+ )
+}
+
+function resolveCheckDiffActivity(record) {
+ return {
+ key: `checkDiff-${record.id || record.orderCode || record.updateTime || ''}`,
+ route: '/manager/checkDiff',
+ source: '鐩樼偣宸紓',
+ title: record.orderCode || '鏈懡鍚嶅樊寮傚崟',
+ summary: `${record.exceStatus$ || '--'} 路 ${record.areaName || '鏈垎閰嶅簱鍖�'}`,
+ time: record.updateTime$ || record.createTime$ || '--',
+ sortTime: record.updateTime || record.createTime || ''
+ }
+}
+
+function resolveQlyInspectActivity(record) {
+ return {
+ key: `qlyInspect-${record.id || record.code || record.updateTime || ''}`,
+ route: '/manager/qlyInspect',
+ source: '璐ㄦ璁板綍',
+ title: record.code || '鏈懡鍚嶈川妫�鍗�',
+ summary: `${record.isptStatus$ || '--'} 路 ${record.asnCode || '鏈叧鑱旈�氱煡鍗�'}`,
+ time: record.updateTime$ || record.createTime$ || '--',
+ sortTime: record.updateTime || record.createTime || ''
+ }
+}
+
+function resolveFreezeActivity(record) {
+ return {
+ key: `freeze-${record.id || record.locCode || record.updateTime || ''}`,
+ route: '/manager/freeze',
+ source: '鍐荤粨搴撳瓨',
+ title: record.locCode || '鏈懡鍚嶅簱浣�',
+ summary: `${record.maktx || '--'} 路 鍋滄粸 ${record.deadTime || 0} 澶ー,
+ time: record.updateTime$ || record.createTime$ || '--',
+ sortTime: record.updateTime || record.createTime || ''
+ }
+}
+
+function resolveLocReviseActivity(record) {
+ return {
+ key: `locRevise-${record.id || record.code || record.updateTime || ''}`,
+ route: '/manager/locRevise',
+ source: '搴撲綅璋冩暣',
+ title: record.code || '鏈懡鍚嶈皟鏁村崟',
+ summary: `${record.exceStatus$ || '--'} 路 ${record.areaName || '鏈垎閰嶅簱鍖�'}`,
+ time: record.updateTime$ || record.createTime$ || '--',
+ sortTime: record.updateTime || record.createTime || ''
+ }
+}
+
+function buildAbnormalOverviewCards(overviewState) {
+ return ABNORMAL_CARD_DEFINITIONS.map((item) => {
+ const state = overviewState[item.key] || { total: 0, records: [] }
+ const latestRecord = Array.isArray(state.records) ? state.records[0] : null
+ return {
+ ...item,
+ total: Number(state.total || 0),
+ subtitle: latestRecord?.updateTime$ || latestRecord?.createTime$ || item.emptyHint,
+ badgeText: latestRecord ? '鏈�杩戞洿鏂�' : '褰撳墠鐘舵��'
+ }
+ })
+}
+
+function buildAbnormalEntryItems(overviewState) {
+ return ABNORMAL_CARD_DEFINITIONS.map((item) => {
+ const state = overviewState[item.key] || { total: 0, records: [] }
+ const latestRecord = Array.isArray(state.records) ? state.records[0] : null
+ return {
+ key: item.key,
+ title: item.title,
+ route: item.route,
+ icon: item.icon,
+ count: Number(state.total || 0),
+ description:
+ latestRecord?.orderCode ||
+ latestRecord?.code ||
+ latestRecord?.locCode ||
+ latestRecord?.maktx ||
+ item.emptyHint
+ }
+ })
+}
+
+function buildAbnormalActivityItems(overviewState) {
+ const activities = []
+ const sourceResolvers = {
+ checkDiff: resolveCheckDiffActivity,
+ qlyInspect: resolveQlyInspectActivity,
+ freeze: resolveFreezeActivity,
+ locRevise: resolveLocReviseActivity
+ }
+
+ Object.entries(sourceResolvers).forEach(([key, resolver]) => {
+ const records = overviewState[key]?.records || []
+ records.slice(0, 3).forEach((record) => {
+ activities.push(resolver(record))
+ })
+ })
+
+ return activities
+ .sort((left, right) => String(right.sortTime).localeCompare(String(left.sortTime)))
+ .slice(0, 8)
+}
+
+export {
+ ABNORMAL_CARD_DEFINITIONS,
+ ABNORMAL_PAGE_TITLE,
+ buildAbnormalActivityItems,
+ buildAbnormalEntryItems,
+ buildAbnormalOverviewCards,
+ createAbnormalOverviewState,
+ normalizeSummaryResponse
+}
diff --git a/rsf-design/src/views/abnormal/index.vue b/rsf-design/src/views/abnormal/index.vue
new file mode 100644
index 0000000..c6bdf38
--- /dev/null
+++ b/rsf-design/src/views/abnormal/index.vue
@@ -0,0 +1,153 @@
+<template>
+ <div class="art-full-height flex flex-col gap-6">
+ <section class="grid gap-6 md:grid-cols-2 xl:grid-cols-4" v-loading="sectionLoading.summary">
+ <div
+ v-for="item in overviewCards"
+ :key="item.key"
+ class="art-card flex cursor-pointer items-start justify-between rounded-3xl px-7 py-6"
+ @click="navigateTo(item.route)"
+ >
+ <div class="min-w-0 pr-6">
+ <p class="text-sm font-medium text-g-700">{{ item.title }}</p>
+ <ArtCountTo class="mt-3 block text-[2.3rem] font-semibold leading-none text-g-900" :target="item.total" :duration="1200" />
+ <div class="mt-4 flex items-center gap-2 text-sm">
+ <span class="text-g-500">{{ item.badgeText }}</span>
+ <span class="text-g-600">{{ item.subtitle }}</span>
+ </div>
+ </div>
+ <div class="flex size-13 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass">
+ <ArtSvgIcon :icon="item.icon" class="text-2xl" :class="item.iconClass" />
+ </div>
+ </div>
+ </section>
+
+ <section class="grid gap-6 xl:grid-cols-[0.92fr_1.08fr]">
+ <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.summary">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>寮傚父鍏ュ彛</h4>
+ <p>鑱氬悎鐜版湁寮傚父鐩稿叧鐪熷疄涓氬姟椤�</p>
+ </div>
+ </div>
+
+ <div class="mt-3 grid gap-3">
+ <button
+ v-for="item in entryItems"
+ :key="item.key"
+ class="flex items-center justify-between rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-4 py-4 text-left transition hover:border-[var(--el-color-primary-light-5)]"
+ type="button"
+ @click="navigateTo(item.route)"
+ >
+ <div class="flex min-w-0 items-center gap-3">
+ <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-[var(--el-color-primary-light-9)]">
+ <ArtSvgIcon :icon="item.icon" class="text-xl text-[var(--el-color-primary)]" />
+ </div>
+ <div class="min-w-0">
+ <p class="truncate text-sm font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
+ <p class="mt-1 truncate text-xs text-g-500">{{ item.description }}</p>
+ </div>
+ </div>
+ <div class="pl-3 text-right">
+ <p class="text-2xl font-semibold text-[var(--art-gray-900)]">{{ item.count }}</p>
+ <p class="mt-1 text-xs text-g-500">鏉¤褰�</p>
+ </div>
+ </button>
+ </div>
+ </div>
+
+ <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.summary">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>鏈�杩戝紓甯稿姩鎬�</h4>
+ <p>鏉ヨ嚜鐩樼偣宸紓銆佽川妫�銆佸喕缁撳簱瀛樸�佸簱浣嶈皟鏁�</p>
+ </div>
+ </div>
+
+ <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden">
+ <ElScrollbar>
+ <div
+ v-for="item in activityItems"
+ :key="item.key"
+ class="flex items-start gap-4 border-b border-g-300 py-4 last:border-b-0"
+ >
+ <div class="flex flex-col items-center pt-1">
+ <span class="size-3 rounded-full bg-[var(--el-color-danger)]"></span>
+ <span class="mt-2 min-h-10 w-px bg-[var(--art-border-color)]"></span>
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="flex items-center gap-2">
+ <p class="truncate text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
+ <ElTag size="small" effect="light" type="danger">{{ item.source }}</ElTag>
+ </div>
+ <p class="mt-2 text-sm text-g-600">{{ item.summary }}</p>
+ <p class="mt-2 text-xs text-g-500">{{ item.time }}</p>
+ </div>
+ <ElButton text type="primary" @click="navigateTo(item.route)">鏌ョ湅</ElButton>
+ </div>
+
+ <ElEmpty v-if="!activityItems.length" description="鏆傛棤寮傚父鍔ㄦ��" :image-size="88" />
+ </ElScrollbar>
+ </div>
+ </div>
+ </section>
+ </div>
+</template>
+
+<script setup>
+ import { useRouter } from 'vue-router'
+ import { withRequestGuard } from '@/utils/sys/requestGuard'
+ import { fetchCheckDiffPage } from '@/api/check-diff'
+ import { fetchQlyInspectPage } from '@/api/qly-inspect'
+ import { fetchFreezePage } from '@/api/freeze'
+ import { fetchLocRevisePage } from '@/api/loc-revise'
+ import {
+ ABNORMAL_PAGE_TITLE,
+ buildAbnormalActivityItems,
+ buildAbnormalEntryItems,
+ buildAbnormalOverviewCards,
+ createAbnormalOverviewState,
+ normalizeSummaryResponse
+ } from './abnormalPage.helpers'
+
+ defineOptions({ name: 'AbnormalManage' })
+
+ const router = useRouter()
+ const sectionLoading = reactive({
+ summary: false
+ })
+ const overviewState = ref(createAbnormalOverviewState())
+
+ const overviewCards = computed(() => buildAbnormalOverviewCards(overviewState.value))
+ const entryItems = computed(() => buildAbnormalEntryItems(overviewState.value))
+ const activityItems = computed(() => buildAbnormalActivityItems(overviewState.value))
+
+ onMounted(() => {
+ document.title = `${ABNORMAL_PAGE_TITLE} - RSF Design`
+ void loadOverview()
+ })
+
+ async function loadOverview() {
+ sectionLoading.summary = true
+
+ const [checkDiffResponse, qlyInspectResponse, freezeResponse, locReviseResponse] = await Promise.all([
+ withRequestGuard(fetchCheckDiffPage({ current: 1, pageSize: 5 }), { records: [], total: 0 }),
+ withRequestGuard(fetchQlyInspectPage({ current: 1, pageSize: 5 }), { records: [], total: 0 }),
+ withRequestGuard(fetchFreezePage({ current: 1, pageSize: 5 }), { records: [], total: 0 }),
+ withRequestGuard(fetchLocRevisePage({ current: 1, pageSize: 5 }), { records: [], total: 0 })
+ ])
+
+ overviewState.value = {
+ checkDiff: normalizeSummaryResponse(checkDiffResponse),
+ qlyInspect: normalizeSummaryResponse(qlyInspectResponse),
+ freeze: normalizeSummaryResponse(freezeResponse),
+ locRevise: normalizeSummaryResponse(locReviseResponse)
+ }
+
+ sectionLoading.summary = false
+ }
+
+ function navigateTo(path) {
+ if (!path) return
+ router.push(path)
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/bas-container/basContainerPage.helpers.js b/rsf-design/src/views/basic-info/bas-container/basContainerPage.helpers.js
new file mode 100644
index 0000000..8a56e0d
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-container/basContainerPage.helpers.js
@@ -0,0 +1,342 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const BAS_CONTAINER_REPORT_TITLE = '瀹瑰櫒瑙勫垯鎶ヨ〃'
+export const BAS_CONTAINER_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+export function createBasContainerSearchState() {
+ return {
+ condition: '',
+ code: '',
+ containerType: '',
+ codeType: '',
+ areas: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createBasContainerFormState() {
+ return {
+ id: void 0,
+ code: '',
+ containerType: void 0,
+ codeType: '',
+ areas: [],
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getBasContainerStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getBasContainerPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getBasContainerStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildBasContainerSearchParams(params = {}) {
+ const result = {}
+
+ ;['condition', 'code', 'codeType', 'areas', 'memo', 'timeStart', 'timeEnd'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.containerType !== '' && params.containerType !== null && params.containerType !== undefined) {
+ result.containerType = Number(params.containerType)
+ }
+
+ if (params.status !== '' && params.status !== null && params.status !== undefined) {
+ result.status = Number(params.status)
+ }
+
+ return result
+}
+
+export function buildBasContainerPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildBasContainerSearchParams(params)
+ }
+}
+
+export function normalizeBasContainerAreas(areas = []) {
+ if (!Array.isArray(areas)) {
+ return []
+ }
+
+ return areas
+ .map((item, index) => normalizeBasContainerAreaRecord(item, index))
+ .filter(Boolean)
+ .sort((left, right) => {
+ const leftSort = normalizeNumber(left.sort, Number.MAX_SAFE_INTEGER)
+ const rightSort = normalizeNumber(right.sort, Number.MAX_SAFE_INTEGER)
+ return leftSort - rightSort
+ })
+ .map((item, index) => ({
+ id: item.id,
+ sort: index + 1
+ }))
+}
+
+export function buildBasContainerSavePayload(formData = {}) {
+ const areas = normalizeBasContainerAreas(formData.areas)
+ const payload = {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ code: normalizeText(formData.code),
+ ...(formData.containerType !== void 0 && formData.containerType !== null && formData.containerType !== ''
+ ? { containerType: Number(formData.containerType) }
+ : {}),
+ codeType: normalizeText(formData.codeType),
+ areas,
+ status: formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo)
+ }
+
+ if (!payload.areas.length) {
+ delete payload.areas
+ }
+
+ return payload
+}
+
+export function buildBasContainerDialogModel(record = {}) {
+ return {
+ ...createBasContainerFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ code: normalizeText(record.code || ''),
+ containerType:
+ record.containerType !== void 0 && record.containerType !== null && record.containerType !== ''
+ ? Number(record.containerType)
+ : record.containerType === 0
+ ? 0
+ : void 0,
+ codeType: normalizeText(record.codeType || ''),
+ areas: normalizeBasContainerAreas(record.areas),
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeBasContainerDetailRecord(record = {}, resolveAreaLabel) {
+ const areas = normalizeBasContainerAreas(record.areas)
+ const statusMeta = getBasContainerStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ areas,
+ containerTypeText: normalizeText(record.containerType$ || record.containerTypeText || ''),
+ areasText: buildBasContainerAreasText(areas, record.areas, resolveAreaLabel),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ areasIds: areas.map((item) => item.id)
+ }
+}
+
+export function normalizeBasContainerListRow(record = {}, resolveAreaLabel) {
+ return normalizeBasContainerDetailRecord(record, resolveAreaLabel)
+}
+
+export function buildBasContainerPrintRows(records = [], resolveAreaLabel) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeBasContainerListRow(record, resolveAreaLabel))
+}
+
+export function buildBasContainerReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = BAS_CONTAINER_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: BAS_CONTAINER_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...BAS_CONTAINER_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function buildBasContainerContainerTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.label || item.name || item.dictLabel || item.value || '')
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveBasContainerAreaOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+
+ const value = item.id ?? item.areaId ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+
+ return {
+ value: Number(value),
+ label: normalizeText(item.name || item.areaName || item.code || item.areaCode || `搴撳尯 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+function normalizeBasContainerAreaRecord(item, index) {
+ if (item === null || item === undefined) {
+ return null
+ }
+
+ if (typeof item === 'object') {
+ const id = normalizeNumber(item.id ?? item.areaId ?? item.value, void 0)
+ if (id === void 0) {
+ return null
+ }
+ return {
+ id,
+ sort: normalizeNumber(item.sort, index + 1),
+ name: normalizeText(item.name || item.areaName || item.label || ''),
+ code: normalizeText(item.code || item.areaCode || ''),
+ memo: normalizeText(item.memo || '')
+ }
+ }
+
+ const id = normalizeNumber(item, void 0)
+ if (id === void 0) {
+ return null
+ }
+ return {
+ id,
+ sort: index + 1,
+ name: '',
+ code: '',
+ memo: ''
+ }
+}
+
+function buildBasContainerAreasText(areas = [], fallbackAreas = [], resolveAreaLabel) {
+ const fallbackLabelMap = new Map(
+ (Array.isArray(fallbackAreas) ? fallbackAreas : [])
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const id = normalizeNumber(item.id ?? item.areaId ?? item.value, void 0)
+ if (id === void 0) {
+ return null
+ }
+ return [id, normalizeText(item.name || item.areaName || item.code || item.areaCode || '')]
+ })
+ .filter(Boolean)
+ )
+
+ const orderedLabels = (Array.isArray(areas) ? areas : [])
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return ''
+ }
+ if (typeof resolveAreaLabel === 'function') {
+ const resolvedLabel = normalizeText(resolveAreaLabel(item.id))
+ if (resolvedLabel) {
+ return resolvedLabel
+ }
+ }
+ const fallbackLabel = fallbackLabelMap.get(item.id)
+ if (fallbackLabel) {
+ return fallbackLabel
+ }
+ return normalizeText(item.name || item.areaName || item.code || item.areaCode || item.id)
+ })
+ .filter(Boolean)
+
+ if (orderedLabels.length) {
+ return orderedLabels.join('銆�')
+ }
+
+ const fallbackLabels = (Array.isArray(fallbackAreas) ? fallbackAreas : [])
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return ''
+ }
+ return normalizeText(item.name || item.areaName || item.code || item.areaCode || item.id)
+ })
+ .filter(Boolean)
+
+ const labels = fallbackLabels
+ return labels.length ? labels.join('銆�') : ''
+}
diff --git a/rsf-design/src/views/basic-info/bas-container/basContainerTable.columns.js b/rsf-design/src/views/basic-info/bas-container/basContainerTable.columns.js
new file mode 100644
index 0000000..662276a
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-container/basContainerTable.columns.js
@@ -0,0 +1,136 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+import { getBasContainerStatusMeta } from './basContainerPage.helpers'
+
+export function createBasContainerTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ resolveAreaLabel,
+ canEdit = true,
+ canDelete = true
+}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'containerTypeText',
+ label: '瀹瑰櫒绫诲瀷',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.containerTypeText || row.containerType$ || '--'
+ },
+ {
+ prop: 'code',
+ label: '鍞竴缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'codeType',
+ label: '鏉$爜绫诲瀷',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.codeType || '--'
+ },
+ {
+ prop: 'areasText',
+ label: '鍙叆搴撳尯',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => {
+ if (typeof resolveAreaLabel === 'function' && Array.isArray(row.areas) && row.areas.length) {
+ const labels = row.areas
+ .map((item) => resolveAreaLabel(item.id || item.areaId || item.value))
+ .filter(Boolean)
+ if (labels.length) {
+ return labels.join('銆�')
+ }
+ }
+ return row.areasText || '--'
+ }
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getBasContainerStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || row.updateBy$ || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || row.createBy$ || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || row.createTime$ || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/bas-container/index.vue b/rsf-design/src/views/basic-info/bas-container/index.vue
new file mode 100644
index 0000000..16f81b6
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-container/index.vue
@@ -0,0 +1,400 @@
+<template>
+ <div class="bas-container-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板瀹瑰櫒瑙勫垯</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <BasContainerDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :container-data="currentContainerData"
+ :area-options="areaOptions"
+ :container-type-options="containerTypeOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <BasContainerDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :resolve-area-label="resolveAreaLabel"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchBasContainerPage,
+ fetchDeleteBasContainer,
+ fetchExportBasContainerReport,
+ fetchGetBasContainerDetail,
+ fetchGetBasContainerMany,
+ fetchSaveBasContainer,
+ fetchUpdateBasContainer,
+ fetchWarehouseAreasList
+ } from '@/api/bas-container'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import BasContainerDialog from './modules/bas-container-dialog.vue'
+ import BasContainerDetailDrawer from './modules/bas-container-detail-drawer.vue'
+ import { createBasContainerTableColumns } from './basContainerTable.columns'
+ import {
+ BAS_CONTAINER_REPORT_STYLE,
+ BAS_CONTAINER_REPORT_TITLE,
+ buildBasContainerDialogModel,
+ buildBasContainerPageQueryParams,
+ buildBasContainerPrintRows,
+ buildBasContainerReportMeta,
+ buildBasContainerSavePayload,
+ buildBasContainerSearchParams,
+ buildBasContainerContainerTypeOptions,
+ createBasContainerSearchState,
+ getBasContainerPaginationKey,
+ normalizeBasContainerDetailRecord,
+ normalizeBasContainerListRow,
+ resolveBasContainerAreaOptions
+ } from './basContainerPage.helpers'
+
+ defineOptions({ name: 'BasContainer' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createBasContainerSearchState())
+ const areaOptions = ref([])
+ const containerTypeOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = BAS_CONTAINER_REPORT_TITLE
+ const reportQueryParams = computed(() => buildBasContainerSearchParams(searchForm.value))
+ const areaLabelMap = computed(
+ () =>
+ new Map(
+ areaOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+
+ function resolveAreaLabel(id) {
+ return areaLabelMap.value.get(String(id)) || ''
+ }
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ敮涓�缂栫爜/鏉$爜绫诲瀷/澶囨敞'
+ }
+ },
+ {
+ label: '鍞竴缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ敮涓�缂栫爜'
+ }
+ },
+ {
+ label: '瀹瑰櫒绫诲瀷',
+ key: 'containerType',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: containerTypeOptions.value
+ }
+ },
+ {
+ label: '鏉$爜绫诲瀷',
+ key: 'codeType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯鐮佺被鍨�'
+ }
+ },
+ {
+ label: '鍙叆搴撳尯',
+ key: 'areas',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ彲鍏ュ簱鍖�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨寮�濮嬫椂闂�'
+ }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨缁撴潫鏃堕棿'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetBasContainerDetail(row.id), {}, {
+ timeoutMessage: '瀹瑰櫒瑙勫垯璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeBasContainerDetailRecord(detail, resolveAreaLabel)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇瀹瑰櫒瑙勫垯璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetBasContainerDetail(row.id), {}, {
+ timeoutMessage: '瀹瑰櫒瑙勫垯璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇瀹瑰櫒瑙勫垯璇︽儏澶辫触')
+ }
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchBasContainerPage,
+ apiParams: buildBasContainerPageQueryParams(searchForm.value),
+ paginationKey: getBasContainerPaginationKey(),
+ columnsFactory: () =>
+ createBasContainerTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ resolveAreaLabel,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeBasContainerListRow(item, resolveAreaLabel))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentContainerData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildBasContainerDialogModel(),
+ buildEditModel: (record) => buildBasContainerDialogModel(record),
+ buildSavePayload: (formData) => buildBasContainerSavePayload(formData),
+ saveRequest: fetchSaveBasContainer,
+ updateRequest: fetchUpdateBasContainer,
+ deleteRequest: fetchDeleteBasContainer,
+ entityName: '瀹瑰櫒瑙勫垯',
+ resolveRecordLabel: (record) => record?.code || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...BAS_CONTAINER_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetBasContainerMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchBasContainerPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'bas-container.xlsx',
+ requestExport: (payload) =>
+ fetchExportBasContainerReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildBasContainerPrintRows(records, resolveAreaLabel),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildBasContainerReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || BAS_CONTAINER_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildBasContainerSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createBasContainerSearchState())
+ resetSearchParams()
+ }
+
+ async function loadAreaOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '鍙叆搴撳尯鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveBasContainerAreaOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadContainerTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_container_type',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '瀹瑰櫒绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ containerTypeOptions.value = buildBasContainerContainerTypeOptions(defaultResponseAdapter(response).records)
+ }
+
+ onMounted(async () => {
+ await Promise.all([loadAreaOptions(), loadContainerTypeOptions()])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/bas-container/modules/bas-container-areas-editor.vue b/rsf-design/src/views/basic-info/bas-container/modules/bas-container-areas-editor.vue
new file mode 100644
index 0000000..2ce6b2e
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-container/modules/bas-container-areas-editor.vue
@@ -0,0 +1,177 @@
+<template>
+ <div class="space-y-3">
+ <div class="flex flex-wrap items-center gap-2">
+ <ElSelect
+ v-model="selectedAreaId"
+ class="min-w-0 flex-1"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍙叆搴撳尯"
+ >
+ <ElOption
+ v-for="option in availableAreaOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </ElSelect>
+ <ElButton :disabled="selectedAreaId === '' || selectedAreaId === void 0" @click="handleAddArea">
+ 娣诲姞
+ </ElButton>
+ </div>
+
+ <ElEmpty v-if="!selectedAreas.length" description="鏆傛棤鍙叆搴撳尯" />
+
+ <div v-else class="space-y-2">
+ <div
+ v-for="(item, index) in selectedAreas"
+ :key="item.id"
+ class="flex flex-wrap items-center gap-2 rounded-lg border border-[var(--art-border-color)] px-3 py-2"
+ >
+ <div class="flex w-10 shrink-0 items-center justify-center text-sm text-[var(--art-text-secondary)]">
+ {{ index + 1 }}
+ </div>
+ <div class="min-w-0 flex-1">
+ <div class="truncate font-medium text-[var(--art-text-primary)]">
+ {{ resolveAreaLabel(item) }}
+ </div>
+ <div class="text-xs text-[var(--art-text-secondary)]">
+ ID: {{ item.id }}
+ </div>
+ </div>
+ <div class="flex items-center gap-2">
+ <ElInputNumber
+ :model-value="item.sort"
+ :min="1"
+ :controls-position="'right'"
+ class="!w-24"
+ @update:model-value="handleSortChange(item.id, $event)"
+ />
+ <ElButton text :disabled="index === 0" @click="handleMoveUp(index)">涓婄Щ</ElButton>
+ <ElButton text :disabled="index === selectedAreas.length - 1" @click="handleMoveDown(index)">
+ 涓嬬Щ
+ </ElButton>
+ <ElButton text type="danger" @click="handleRemove(item.id)">鍒犻櫎</ElButton>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ const props = defineProps({
+ areaOptions: {
+ type: Array,
+ default: () => []
+ }
+ })
+
+ const modelValue = defineModel({ type: Array, default: () => [] })
+ const selectedAreaId = ref('')
+
+ const selectedAreas = computed(() => normalizeSelectedAreas(modelValue.value))
+ const areaOptionMap = computed(() =>
+ new Map(
+ (Array.isArray(props.areaOptions) ? props.areaOptions : [])
+ .map((item) => normalizeAreaOption(item))
+ .filter(Boolean)
+ .map((item) => [String(item.value), item.label])
+ )
+ )
+ const availableAreaOptions = computed(() => {
+ const selectedIds = new Set(selectedAreas.value.map((item) => String(item.id)))
+ return (Array.isArray(props.areaOptions) ? props.areaOptions : [])
+ .map((item) => normalizeAreaOption(item))
+ .filter((item) => item && !selectedIds.has(String(item.value)))
+ })
+
+ function normalizeAreaOption(option) {
+ if (!option || typeof option !== 'object') {
+ return null
+ }
+ const value = option.value ?? option.id ?? option.areaId
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: String(option.label || option.name || option.areaName || option.code || option.areaCode || value)
+ }
+ }
+
+ function normalizeSelectedAreas(source = []) {
+ if (!Array.isArray(source)) {
+ return []
+ }
+ return source
+ .map((item, index) => {
+ if (item === null || item === void 0) {
+ return null
+ }
+ if (typeof item === 'object') {
+ const id = Number(item.id ?? item.areaId ?? item.value)
+ if (Number.isNaN(id)) return null
+ return {
+ id,
+ sort: Number(item.sort ?? index + 1) || index + 1
+ }
+ }
+ const id = Number(item)
+ if (Number.isNaN(id)) return null
+ return {
+ id,
+ sort: index + 1
+ }
+ })
+ .filter(Boolean)
+ .sort((left, right) => Number(left.sort) - Number(right.sort))
+ }
+
+ function syncModel(rows) {
+ modelValue.value = rows.map((item, index) => ({
+ id: item.id,
+ sort: index + 1
+ }))
+ }
+
+ function resolveAreaLabel(item) {
+ return areaOptionMap.value.get(String(item.id)) || `搴撳尯 ${item.id}`
+ }
+
+ function handleAddArea() {
+ const nextId = Number(selectedAreaId.value)
+ if (!nextId || selectedAreas.value.some((item) => Number(item.id) === nextId)) {
+ return
+ }
+ const nextRows = [...selectedAreas.value, { id: nextId, sort: selectedAreas.value.length + 1 }]
+ syncModel(nextRows)
+ selectedAreaId.value = ''
+ }
+
+ function handleSortChange(id, value) {
+ const nextSort = Number(value) || 1
+ const nextRows = selectedAreas.value.map((item) =>
+ Number(item.id) === Number(id) ? { ...item, sort: nextSort } : { ...item }
+ )
+ nextRows.sort((left, right) => Number(left.sort) - Number(right.sort))
+ syncModel(nextRows)
+ }
+
+ function handleMoveUp(index) {
+ if (index <= 0) return
+ const nextRows = [...selectedAreas.value]
+ ;[nextRows[index - 1], nextRows[index]] = [nextRows[index], nextRows[index - 1]]
+ syncModel(nextRows)
+ }
+
+ function handleMoveDown(index) {
+ if (index >= selectedAreas.value.length - 1) return
+ const nextRows = [...selectedAreas.value]
+ ;[nextRows[index + 1], nextRows[index]] = [nextRows[index], nextRows[index + 1]]
+ syncModel(nextRows)
+ }
+
+ function handleRemove(id) {
+ syncModel(selectedAreas.value.filter((item) => Number(item.id) !== Number(id)))
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/bas-container/modules/bas-container-detail-drawer.vue b/rsf-design/src/views/basic-info/bas-container/modules/bas-container-detail-drawer.vue
new file mode 100644
index 0000000..1ad9d29
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-container/modules/bas-container-detail-drawer.vue
@@ -0,0 +1,83 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="瀹瑰櫒瑙勫垯璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="瀹瑰櫒绫诲瀷">{{ detail.containerTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍞竴缂栫爜">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉$爜绫诲瀷">{{ detail.codeType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="鍙叆搴撳尯" :column="1" border>
+ <ElDescriptionsItem v-if="!areas.length" label="鍒楄〃">鏆傛棤鍙叆搴撳尯</ElDescriptionsItem>
+ <ElDescriptionsItem v-for="item in areas" v-else :key="item.id" :label="`#${item.sort}`">
+ <div class="flex flex-col gap-1">
+ <div class="font-medium text-[var(--art-text-primary)]">
+ {{ resolveAreaLabel(item.id) || item.name || item.code || `搴撳尯 ${item.id}` }}
+ </div>
+ <div class="text-xs text-[var(--art-text-secondary)]">ID: {{ item.id }}</div>
+ </div>
+ </ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ resolveAreaLabel: { type: Function, default: null }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ const areas = computed(() => {
+ if (!Array.isArray(props.detail?.areas)) {
+ return []
+ }
+ return props.detail.areas
+ })
+
+ function resolveAreaLabel(id) {
+ if (typeof props.resolveAreaLabel === 'function') {
+ return String(props.resolveAreaLabel(id) || '').trim()
+ }
+ return ''
+ }
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/bas-container/modules/bas-container-dialog.vue b/rsf-design/src/views/basic-info/bas-container/modules/bas-container-dialog.vue
new file mode 100644
index 0000000..9537098
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-container/modules/bas-container-dialog.vue
@@ -0,0 +1,171 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="960px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ >
+ <template #areas>
+ <BasContainerAreasEditor v-model="form.areas" :area-options="areaOptions" />
+ </template>
+ </ArtForm>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildBasContainerDialogModel, createBasContainerFormState, getBasContainerStatusOptions } from '../basContainerPage.helpers'
+ import BasContainerAreasEditor from './bas-container-areas-editor.vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ containerData: { type: Object, default: () => ({}) },
+ areaOptions: { type: Array, default: () => [] },
+ containerTypeOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createBasContainerFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫瀹瑰櫒瑙勫垯' : '鏂板瀹瑰櫒瑙勫垯'))
+
+ const rules = computed(() => ({
+ containerType: [{ required: true, message: '璇烽�夋嫨瀹瑰櫒绫诲瀷', trigger: 'change' }],
+ code: [{ required: true, message: '璇疯緭鍏ュ敮涓�缂栫爜', trigger: 'blur' }],
+ codeType: [{ required: true, message: '璇疯緭鍏ユ潯鐮佺被鍨�', trigger: 'blur' }],
+ areas: [{ type: 'array', required: true, message: '璇疯嚦灏戦�夋嫨涓�涓彲鍏ュ簱鍖�', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '瀹瑰櫒绫诲瀷',
+ key: 'containerType',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨瀹瑰櫒绫诲瀷',
+ clearable: true,
+ options: props.containerTypeOptions || []
+ }
+ },
+ {
+ label: '鍞竴缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ敮涓�缂栫爜',
+ clearable: true
+ }
+ },
+ {
+ label: '鏉$爜绫诲瀷',
+ key: 'codeType',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ潯鐮佺被鍨�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍙叆搴撳尯',
+ key: 'areas',
+ type: 'input',
+ span: 24
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getBasContainerStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildBasContainerDialogModel(props.containerData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createBasContainerFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.containerData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js b/rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js
new file mode 100644
index 0000000..658dddb
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js
@@ -0,0 +1,474 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+const TYPE_OPTIONS = [
+ { label: '鏅鸿兘绔欑偣', value: 0 },
+ { label: '鏅�氱珯鐐�', value: 1 }
+]
+
+const BINARY_OPTIONS = [
+ { label: '鍚�', value: 0 },
+ { label: '鏄�', value: 1 }
+]
+
+const STATUS_OPTIONS = [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+]
+
+export const BAS_STATION_AREA_REPORT_TITLE = '绔欑偣鍖哄煙鎶ヨ〃'
+export const BAS_STATION_AREA_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeBooleanText(value) {
+ if (value === 1 || value === '1' || value === true || value === '鏄�') {
+ return '鏄�'
+ }
+ if (value === 0 || value === '0' || value === false || value === '鍚�') {
+ return '鍚�'
+ }
+ return normalizeText(value) || '--'
+}
+
+function normalizeIdArray(values = []) {
+ if (Array.isArray(values)) {
+ return values
+ .map((item) => normalizeNumber(item, void 0))
+ .filter((item) => item !== void 0 && item !== null)
+ }
+
+ if (typeof values === 'string' && values.trim()) {
+ return values
+ .split(',')
+ .map((item) => normalizeNumber(item, void 0))
+ .filter((item) => item !== void 0 && item !== null)
+ }
+
+ return []
+}
+
+function normalizeStringArray(values = []) {
+ if (Array.isArray(values)) {
+ return values.map((item) => normalizeText(item)).filter(Boolean)
+ }
+
+ if (typeof values === 'string' && values.trim()) {
+ return values
+ .split(',')
+ .map((item) => normalizeText(item))
+ .filter(Boolean)
+ }
+
+ return []
+}
+
+function buildJoinedText(values = [], resolver) {
+ const labels = values
+ .map((value) => {
+ if (typeof resolver === 'function') {
+ return normalizeText(resolver(value))
+ }
+ return normalizeText(value)
+ })
+ .filter(Boolean)
+
+ return labels.length ? labels.join('銆�') : '--'
+}
+
+function getStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function createBasStationAreaSearchState() {
+ return {
+ condition: '',
+ stationAreaName: '',
+ stationAreaId: '',
+ type: '',
+ inAble: '',
+ outAble: '',
+ useStatus: '',
+ area: '',
+ isCrossZone: '',
+ crossZoneArea: '',
+ isWcs: '',
+ wcsData: '',
+ containerType: '',
+ barcode: '',
+ autoTransfer: '',
+ stationAlias: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function createBasStationAreaFormState() {
+ return {
+ id: void 0,
+ stationAreaName: '',
+ stationAreaId: '',
+ type: void 0,
+ inAble: 0,
+ outAble: 0,
+ useStatus: '',
+ area: void 0,
+ isCrossZone: 0,
+ crossZoneArea: [],
+ isWcs: 0,
+ wcsData: '',
+ containerType: [],
+ barcode: '',
+ autoTransfer: 0,
+ stationAlias: [],
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getBasStationAreaPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getBasStationAreaTypeOptions() {
+ return TYPE_OPTIONS
+}
+
+export function getBasStationAreaBinaryOptions() {
+ return BINARY_OPTIONS
+}
+
+export function getBasStationAreaStatusOptions() {
+ return STATUS_OPTIONS
+}
+
+export function resolveBasStationAreaWarehouseAreaOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.areaId ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.name || item.areaName || item.code || item.areaCode || `搴撳尯 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveBasStationAreaStationOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.stationId ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.stationName || item.stationId || item.name || `绔欑偣 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveBasStationAreaContainerTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.label || item.name || item.dictLabel || item.value || '')
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveBasStationAreaUseStatusOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: normalizeText(item.value ?? value),
+ label: normalizeText(item.label || item.name || item.dictLabel || item.value || '')
+ }
+ })
+ .filter(Boolean)
+}
+
+export function buildBasStationAreaSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ stationAreaName: normalizeText(params.stationAreaName),
+ stationAreaId: normalizeText(params.stationAreaId),
+ type:
+ params.type !== undefined && params.type !== null && params.type !== ''
+ ? Number(params.type)
+ : void 0,
+ inAble:
+ params.inAble !== undefined && params.inAble !== null && params.inAble !== ''
+ ? Number(params.inAble)
+ : void 0,
+ outAble:
+ params.outAble !== undefined && params.outAble !== null && params.outAble !== ''
+ ? Number(params.outAble)
+ : void 0,
+ useStatus: normalizeText(params.useStatus),
+ area:
+ params.area !== undefined && params.area !== null && params.area !== ''
+ ? Number(params.area)
+ : void 0,
+ isCrossZone:
+ params.isCrossZone !== undefined && params.isCrossZone !== null && params.isCrossZone !== ''
+ ? Number(params.isCrossZone)
+ : void 0,
+ crossZoneArea: normalizeText(params.crossZoneArea),
+ isWcs:
+ params.isWcs !== undefined && params.isWcs !== null && params.isWcs !== ''
+ ? Number(params.isWcs)
+ : void 0,
+ wcsData: normalizeText(params.wcsData),
+ containerType: normalizeText(params.containerType),
+ barcode: normalizeText(params.barcode),
+ autoTransfer:
+ params.autoTransfer !== undefined && params.autoTransfer !== null && params.autoTransfer !== ''
+ ? Number(params.autoTransfer)
+ : void 0,
+ stationAlias: normalizeText(params.stationAlias),
+ memo: normalizeText(params.memo),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildBasStationAreaPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildBasStationAreaSearchParams(params)
+ }
+}
+
+function normalizeIdValue(value) {
+ if (value === '' || value === null || value === undefined) {
+ return void 0
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? void 0 : numberValue
+}
+
+function resolveOptionText(ids = [], resolver, fallback = []) {
+ const fallbackList = normalizeStringArray(fallback)
+ const resolvedList = normalizeIdArray(ids).map((id, index) => {
+ if (typeof resolver === 'function') {
+ const text = normalizeText(resolver(id))
+ if (text) {
+ return text
+ }
+ }
+ return fallbackList[index] || normalizeText(id)
+ })
+ return buildJoinedText(resolvedList)
+}
+
+export function buildBasStationAreaSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ stationAreaName: normalizeText(formData.stationAreaName) || '',
+ stationAreaId: normalizeText(formData.stationAreaId) || '',
+ ...(formData.type !== void 0 && formData.type !== null && formData.type !== ''
+ ? { type: Number(formData.type) }
+ : {}),
+ ...(formData.inAble !== void 0 && formData.inAble !== null && formData.inAble !== ''
+ ? { inAble: Number(formData.inAble) }
+ : {}),
+ ...(formData.outAble !== void 0 && formData.outAble !== null && formData.outAble !== ''
+ ? { outAble: Number(formData.outAble) }
+ : {}),
+ useStatus: normalizeText(formData.useStatus) || '',
+ ...(formData.area !== void 0 && formData.area !== null && formData.area !== ''
+ ? { area: Number(formData.area) }
+ : {}),
+ ...(formData.isCrossZone !== void 0 && formData.isCrossZone !== null && formData.isCrossZone !== ''
+ ? { isCrossZone: Number(formData.isCrossZone) }
+ : {}),
+ ...(Array.isArray(formData.crossZoneArea) && formData.crossZoneArea.length
+ ? { crossZoneArea: normalizeIdArray(formData.crossZoneArea) }
+ : {}),
+ ...(formData.isWcs !== void 0 && formData.isWcs !== null && formData.isWcs !== ''
+ ? { isWcs: Number(formData.isWcs) }
+ : {}),
+ wcsData: normalizeText(formData.wcsData) || '',
+ ...(Array.isArray(formData.containerType) && formData.containerType.length
+ ? { containerType: normalizeIdArray(formData.containerType) }
+ : {}),
+ barcode: normalizeText(formData.barcode) || '',
+ ...(formData.autoTransfer !== void 0 && formData.autoTransfer !== null && formData.autoTransfer !== ''
+ ? { autoTransfer: Number(formData.autoTransfer) }
+ : {}),
+ ...(Array.isArray(formData.stationAlias) && formData.stationAlias.length
+ ? { stationAlias: normalizeIdArray(formData.stationAlias).map((item) => String(item)) }
+ : {}),
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildBasStationAreaDialogModel(record = {}, resolvers = {}) {
+ return {
+ ...createBasStationAreaFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ stationAreaName: normalizeText(record.stationAreaName || ''),
+ stationAreaId: normalizeText(record.stationAreaId || ''),
+ type: normalizeIdValue(record.type),
+ inAble: record.inAble !== void 0 && record.inAble !== null ? Number(record.inAble) : 0,
+ outAble: record.outAble !== void 0 && record.outAble !== null ? Number(record.outAble) : 0,
+ useStatus: normalizeText(record.useStatus || ''),
+ area: normalizeIdValue(record.area),
+ isCrossZone: record.isCrossZone !== void 0 && record.isCrossZone !== null ? Number(record.isCrossZone) : 0,
+ crossZoneArea: normalizeIdArray(record.crossZoneArea),
+ isWcs: record.isWcs !== void 0 && record.isWcs !== null ? Number(record.isWcs) : 0,
+ wcsData: normalizeText(record.wcsData || ''),
+ containerType: normalizeIdArray(record.containerType ?? record.containerTypes),
+ barcode: normalizeText(record.barcode || ''),
+ autoTransfer: record.autoTransfer !== void 0 && record.autoTransfer !== null ? Number(record.autoTransfer) : 0,
+ stationAlias: normalizeIdArray(record.stationAlias),
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeBasStationAreaDetailRecord(record = {}, resolvers = {}) {
+ const statusMeta = getStatusMeta(record.statusBool ?? record.status)
+ const typeValue = record.type$ ?? record.type
+ const areaId = record.area ?? record.areaId ?? record.area$
+ const crossZoneAreaIds = normalizeIdArray(record.crossZoneArea)
+ const containerTypeIds = normalizeIdArray(record.containerType ?? record.containerTypes)
+ const stationAliasIds = normalizeStringArray(record.stationAlias)
+ const stationAliasNames = normalizeStringArray(record.stationAliasStaNo)
+
+ return {
+ ...record,
+ id: normalizeIdValue(record.id),
+ stationAreaName: normalizeText(record.stationAreaName) || '--',
+ stationAreaId: normalizeText(record.stationAreaId) || '--',
+ type: normalizeIdValue(record.type),
+ typeText: normalizeText(
+ record.type$ || record.typeText || resolvers.resolveTypeLabel?.(typeValue) || typeValue
+ ) || '--',
+ inAble: normalizeIdValue(record.inAble),
+ inAbleText: normalizeBooleanText(record.inAble),
+ outAble: normalizeIdValue(record.outAble),
+ outAbleText: normalizeBooleanText(record.outAble),
+ useStatus: normalizeText(record.useStatus),
+ useStatusText: normalizeText(record.useStatus$ || record.useStatusText || resolvers.resolveUseStatusLabel?.(record.useStatus) || record.useStatus) || '--',
+ area: normalizeIdValue(areaId),
+ areaText: normalizeText(record.area$ || record.areaText || resolvers.resolveAreaLabel?.(areaId) || '') || '--',
+ isCrossZone: normalizeIdValue(record.isCrossZone),
+ isCrossZoneText: normalizeBooleanText(record.isCrossZone),
+ crossZoneArea: crossZoneAreaIds,
+ crossZoneAreaText:
+ resolveOptionText(crossZoneAreaIds, resolvers.resolveCrossZoneAreaLabel, record.crossZoneAreaText || []) || '--',
+ isWcs: normalizeIdValue(record.isWcs),
+ isWcsText: normalizeBooleanText(record.isWcs),
+ wcsData: normalizeText(record.wcsData) || '--',
+ containerType: containerTypeIds,
+ containerTypeText:
+ resolveOptionText(containerTypeIds, resolvers.resolveContainerTypeLabel, record.containerTypesText || []) || '--',
+ barcode: normalizeText(record.barcode) || '--',
+ autoTransfer: normalizeIdValue(record.autoTransfer),
+ autoTransferText: normalizeBooleanText(record.autoTransfer),
+ stationAlias: stationAliasIds,
+ stationAliasText:
+ resolveOptionText(
+ stationAliasIds,
+ resolvers.resolveStationAliasLabel,
+ stationAliasNames.length ? stationAliasNames : record.stationAliasText || []
+ ) || '--',
+ status: normalizeIdValue(record.status),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo) || '--',
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeBasStationAreaListRow(record = {}, resolvers = {}) {
+ return normalizeBasStationAreaDetailRecord(record, resolvers)
+}
diff --git a/rsf-design/src/views/basic-info/bas-station-area/basStationAreaTable.columns.js b/rsf-design/src/views/basic-info/bas-station-area/basStationAreaTable.columns.js
new file mode 100644
index 0000000..e5284e9
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station-area/basStationAreaTable.columns.js
@@ -0,0 +1,176 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getBasStationAreaStatusOptions } from './basStationAreaPage.helpers'
+
+export function createBasStationAreaTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'stationAreaId',
+ label: '绔欑偣鍖哄煙缂栧彿',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.stationAreaId || '--'
+ },
+ {
+ prop: 'stationAreaName',
+ label: '绔欑偣鍖哄煙鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.stationAreaName || '--'
+ },
+ {
+ prop: 'typeText',
+ label: '绔欑偣绫诲瀷',
+ width: 120,
+ align: 'center',
+ formatter: (row) => row.typeText || '--'
+ },
+ {
+ prop: 'areaText',
+ label: '鎵�灞炲簱鍖�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.areaText || '--'
+ },
+ {
+ prop: 'crossZoneAreaText',
+ label: '鍙法鍖哄尯鍩�',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.crossZoneAreaText || '--'
+ },
+ {
+ prop: 'containerTypeText',
+ label: '瀹瑰櫒绫诲瀷',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.containerTypeText || '--'
+ },
+ {
+ prop: 'stationAliasText',
+ label: '绔欑偣鍒悕',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.stationAliasText || '--'
+ },
+ {
+ prop: 'inAbleText',
+ label: '鍙叆',
+ width: 84,
+ align: 'center',
+ formatter: (row) => row.inAbleText || '--'
+ },
+ {
+ prop: 'outAbleText',
+ label: '鍙嚭',
+ width: 84,
+ align: 'center',
+ formatter: (row) => row.outAbleText || '--'
+ },
+ {
+ prop: 'isCrossZoneText',
+ label: '璺ㄥ尯',
+ width: 84,
+ align: 'center',
+ formatter: (row) => row.isCrossZoneText || '--'
+ },
+ {
+ prop: 'isWcsText',
+ label: 'WCS',
+ width: 84,
+ align: 'center',
+ formatter: (row) => row.isWcsText || '--'
+ },
+ {
+ prop: 'autoTransferText',
+ label: '鑷姩璋冩嫧',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.autoTransferText || '--'
+ },
+ {
+ prop: 'useStatusText',
+ label: '浣跨敤鐘舵��',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.useStatusText || '--'
+ },
+ {
+ prop: 'barcode',
+ label: '鏉$爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.barcode || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const status = getBasStationAreaStatusOptions().find((item) => Number(item.value) === Number(row.status))
+ const text = status?.label || row.statusText || '--'
+ const type = Number(row.status) === 1 ? 'success' : Number(row.status) === 0 ? 'danger' : 'info'
+ return h(ElTag, { type, effect: 'light' }, () => text)
+ }
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
+
diff --git a/rsf-design/src/views/basic-info/bas-station-area/index.vue b/rsf-design/src/views/basic-info/bas-station-area/index.vue
new file mode 100644
index 0000000..7d9c219
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station-area/index.vue
@@ -0,0 +1,459 @@
+<template>
+ <div class="bas-station-area-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板绔欑偣鍖哄煙</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <BasStationAreaDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :bas-station-area-data="currentBasStationAreaData"
+ :area-options="areaOptions"
+ :cross-zone-area-options="crossZoneAreaOptions"
+ :container-type-options="containerTypeOptions"
+ :station-options="stationOptions"
+ :use-status-options="useStatusOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <BasStationAreaDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import { fetchBasStationPage } from '@/api/bas-station'
+ import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
+ import {
+ fetchBasStationAreaDetail,
+ fetchBasStationAreaPage,
+ fetchDeleteBasStationArea,
+ fetchSaveBasStationArea,
+ fetchUpdateBasStationArea
+ } from '@/api/bas-station-area'
+ import BasStationAreaDialog from './modules/bas-station-area-dialog.vue'
+ import BasStationAreaDetailDrawer from './modules/bas-station-area-detail-drawer.vue'
+ import { createBasStationAreaTableColumns } from './basStationAreaTable.columns'
+ import {
+ buildBasStationAreaDialogModel,
+ buildBasStationAreaPageQueryParams,
+ buildBasStationAreaSavePayload,
+ buildBasStationAreaSearchParams,
+ createBasStationAreaSearchState,
+ getBasStationAreaBinaryOptions,
+ getBasStationAreaPaginationKey,
+ getBasStationAreaStatusOptions,
+ getBasStationAreaTypeOptions,
+ normalizeBasStationAreaDetailRecord,
+ normalizeBasStationAreaListRow,
+ resolveBasStationAreaContainerTypeOptions,
+ resolveBasStationAreaStationOptions,
+ resolveBasStationAreaUseStatusOptions,
+ resolveBasStationAreaWarehouseAreaOptions
+ } from './basStationAreaPage.helpers'
+
+ defineOptions({ name: 'BasStationArea' })
+
+ const { hasAuth } = useAuth()
+
+ const searchForm = ref(createBasStationAreaSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const areaOptions = ref([])
+ const crossZoneAreaOptions = ref([])
+ const containerTypeOptions = ref([])
+ const stationOptions = ref([])
+ const useStatusOptions = ref([])
+ let handleDeleteAction = null
+
+ const areaLabelMap = computed(
+ () =>
+ new Map(
+ areaOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+ const containerTypeLabelMap = computed(
+ () =>
+ new Map(
+ containerTypeOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+ const typeLabelMap = computed(
+ () =>
+ new Map(
+ getBasStationAreaTypeOptions()
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+ const stationLabelMap = computed(
+ () =>
+ new Map(
+ stationOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+ const useStatusLabelMap = computed(
+ () =>
+ new Map(
+ useStatusOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+
+ const resolveAreaLabel = (id) => areaLabelMap.value.get(String(id)) || ''
+ const resolveCrossZoneAreaLabel = (id) => areaLabelMap.value.get(String(id)) || ''
+ const resolveContainerTypeLabel = (id) => containerTypeLabelMap.value.get(String(id)) || ''
+ const resolveTypeLabel = (value) => typeLabelMap.value.get(String(value)) || ''
+ const resolveStationAliasLabel = (id) => stationLabelMap.value.get(String(id)) || ''
+ const resolveUseStatusLabel = (value) => useStatusLabelMap.value.get(String(value)) || ''
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐瑰尯鍩熷悕绉�/缂栧彿/澶囨敞'
+ }
+ },
+ {
+ label: '绔欑偣鍖哄煙鍚嶇О',
+ key: 'stationAreaName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐瑰尯鍩熷悕绉�'
+ }
+ },
+ {
+ label: '绔欑偣鍖哄煙缂栧彿',
+ key: 'stationAreaId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐瑰尯鍩熺紪鍙�'
+ }
+ },
+ {
+ label: '绔欑偣绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationAreaTypeOptions()
+ }
+ },
+ {
+ label: '鎵�灞炲簱鍖�',
+ key: 'area',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: areaOptions.value
+ }
+ },
+ {
+ label: '浣跨敤鐘舵��',
+ key: 'useStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: useStatusOptions.value
+ }
+ },
+ {
+ label: '鍙叆',
+ key: 'inAble',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鍙嚭',
+ key: 'outAble',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鏄惁璺ㄥ尯',
+ key: 'isCrossZone',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鏄惁WCS',
+ key: 'isWcs',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鑷姩璋冩嫧',
+ key: 'autoTransfer',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鏉$爜',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯鐮�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationAreaStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchBasStationAreaDetail(row.id), {}, {
+ timeoutMessage: '绔欑偣鍖哄煙璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeBasStationAreaDetailRecord(detail, {
+ resolveAreaLabel,
+ resolveCrossZoneAreaLabel,
+ resolveContainerTypeLabel,
+ resolveTypeLabel,
+ resolveStationAliasLabel,
+ resolveUseStatusLabel
+ })
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇绔欑偣鍖哄煙璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchBasStationAreaDetail(row.id), {}, {
+ timeoutMessage: '绔欑偣鍖哄煙璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇绔欑偣鍖哄煙璇︽儏澶辫触')
+ }
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchBasStationAreaPage,
+ apiParams: buildBasStationAreaPageQueryParams(searchForm.value),
+ paginationKey: getBasStationAreaPaginationKey(),
+ columnsFactory: () =>
+ createBasStationAreaTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) =>
+ normalizeBasStationAreaListRow(item, {
+ resolveAreaLabel,
+ resolveCrossZoneAreaLabel,
+ resolveContainerTypeLabel,
+ resolveTypeLabel,
+ resolveStationAliasLabel,
+ resolveUseStatusLabel
+ })
+ )
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentBasStationAreaData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildBasStationAreaDialogModel(),
+ buildEditModel: (record) => buildBasStationAreaDialogModel(record),
+ buildSavePayload: (formData) => buildBasStationAreaSavePayload(formData),
+ saveRequest: fetchSaveBasStationArea,
+ updateRequest: fetchUpdateBasStationArea,
+ deleteRequest: fetchDeleteBasStationArea,
+ entityName: '绔欑偣鍖哄煙',
+ resolveRecordLabel: (record) => record?.stationAreaName || record?.stationAreaId || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ function handleSearch(params) {
+ replaceSearchParams(buildBasStationAreaSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createBasStationAreaSearchState())
+ resetSearchParams()
+ }
+
+ async function loadAreaOptions() {
+ const response = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ const options = resolveBasStationAreaWarehouseAreaOptions(defaultResponseAdapter(response).records)
+ areaOptions.value = options
+ crossZoneAreaOptions.value = options
+ }
+
+ async function loadStationOptions() {
+ const response = await guardRequestWithMessage(
+ fetchBasStationPage({
+ current: 1,
+ pageSize: 500
+ }, {
+ showErrorMessage: false
+ }),
+ { records: [] },
+ {
+ timeoutMessage: '绔欑偣鍒悕閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ stationOptions.value = resolveBasStationAreaStationOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadContainerTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_container_type',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '瀹瑰櫒绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ containerTypeOptions.value = resolveBasStationAreaContainerTypeOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadUseStatusOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_sta_use_stas',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '浣跨敤鐘舵�侀�夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ useStatusOptions.value = resolveBasStationAreaUseStatusOptions(defaultResponseAdapter(response).records)
+ }
+
+ onMounted(async () => {
+ await Promise.allSettled([
+ loadAreaOptions(),
+ loadStationOptions(),
+ loadContainerTypeOptions(),
+ loadUseStatusOptions()
+ ])
+ await getData()
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-detail-drawer.vue b/rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-detail-drawer.vue
new file mode 100644
index 0000000..ebd2c7c
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-detail-drawer.vue
@@ -0,0 +1,69 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="绔欑偣鍖哄煙璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="绔欑偣鍖哄煙鍚嶇О">{{ detail.stationAreaName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣鍖哄煙缂栧彿">{{ detail.stationAreaId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣绫诲瀷">{{ detail.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵�灞炲簱鍖�">{{ detail.areaText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙法鍖哄簱鍖�" :span="2">{{ detail.crossZoneAreaText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹瑰櫒绫诲瀷" :span="2">{{ detail.containerTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣鍒悕" :span="2">{{ detail.stationAliasText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙叆">{{ detail.inAbleText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙嚭">{{ detail.outAbleText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏄惁璺ㄥ尯">{{ detail.isCrossZoneText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏄惁WCS">{{ detail.isWcsText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑷姩璋冩嫧">{{ detail.autoTransferText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤鐘舵��">{{ detail.useStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉$爜">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="WCS鏁版嵁" :span="2">{{ detail.wcsData || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
+
diff --git a/rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-dialog.vue b/rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-dialog.vue
new file mode 100644
index 0000000..698219f
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-dialog.vue
@@ -0,0 +1,307 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="1080px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildBasStationAreaDialogModel,
+ createBasStationAreaFormState,
+ getBasStationAreaBinaryOptions,
+ getBasStationAreaStatusOptions,
+ getBasStationAreaTypeOptions
+ } from '../basStationAreaPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ basStationAreaData: { type: Object, default: () => ({}) },
+ areaOptions: { type: Array, default: () => [] },
+ crossZoneAreaOptions: { type: Array, default: () => [] },
+ containerTypeOptions: { type: Array, default: () => [] },
+ stationOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createBasStationAreaFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫绔欑偣鍖哄煙' : '鏂板绔欑偣鍖哄煙'))
+
+ const rules = computed(() => ({
+ stationAreaName: [{ required: true, message: '璇疯緭鍏ョ珯鐐瑰尯鍩熷悕绉�', trigger: 'blur' }],
+ stationAreaId: [{ required: true, message: '璇疯緭鍏ョ珯鐐瑰尯鍩熺紪鍙�', trigger: 'blur' }],
+ type: [{ required: true, message: '璇烽�夋嫨绔欑偣绫诲瀷', trigger: 'change' }],
+ area: [{ required: true, message: '璇烽�夋嫨鎵�灞炲簱鍖�', trigger: 'change' }],
+ containerType: [{ type: 'array', required: true, message: '璇烽�夋嫨瀹瑰櫒绫诲瀷', trigger: 'change' }],
+ stationAlias: [{ type: 'array', required: true, message: '璇烽�夋嫨绔欑偣鍒悕', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '绔欑偣鍖哄煙鍚嶇О',
+ key: 'stationAreaName',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ珯鐐瑰尯鍩熷悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '绔欑偣鍖哄煙缂栧彿',
+ key: 'stationAreaId',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ珯鐐瑰尯鍩熺紪鍙�',
+ clearable: true
+ }
+ },
+ {
+ label: '绔欑偣绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨绔欑偣绫诲瀷',
+ clearable: true,
+ options: getBasStationAreaTypeOptions()
+ }
+ },
+ {
+ label: '鎵�灞炲簱鍖�',
+ key: 'area',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鎵�灞炲簱鍖�',
+ clearable: true,
+ filterable: true,
+ options: props.areaOptions || []
+ }
+ },
+ {
+ label: '鍙法鍖哄簱鍖�',
+ key: 'crossZoneArea',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨鍙法鍖哄簱鍖�',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.crossZoneAreaOptions || []
+ }
+ },
+ {
+ label: '瀹瑰櫒绫诲瀷',
+ key: 'containerType',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨瀹瑰櫒绫诲瀷',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.containerTypeOptions || []
+ }
+ },
+ {
+ label: '绔欑偣鍒悕',
+ key: 'stationAlias',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨绔欑偣鍒悕',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.stationOptions || []
+ }
+ },
+ {
+ label: '鍙叆',
+ key: 'inAble',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鍙叆',
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鍙嚭',
+ key: 'outAble',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鍙嚭',
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鏄惁璺ㄥ尯',
+ key: 'isCrossZone',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏄惁璺ㄥ尯',
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鏄惁WCS',
+ key: 'isWcs',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏄惁WCS',
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '鑷姩璋冩嫧',
+ key: 'autoTransfer',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鑷姩璋冩嫧',
+ clearable: true,
+ options: getBasStationAreaBinaryOptions()
+ }
+ },
+ {
+ label: '浣跨敤鐘舵��',
+ key: 'useStatus',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨浣跨敤鐘舵��',
+ clearable: true,
+ filterable: true,
+ options: props.useStatusOptions || []
+ }
+ },
+ {
+ label: 'WCS鏁版嵁',
+ key: 'wcsData',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏CS鏁版嵁',
+ clearable: true
+ }
+ },
+ {
+ label: '鏉$爜',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ潯鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getBasStationAreaStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const resetForm = () => {
+ Object.assign(form, createBasStationAreaFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const loadFormData = () => {
+ Object.assign(form, buildBasStationAreaDialogModel(props.basStationAreaData))
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.basStationAreaData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
+
diff --git a/rsf-design/src/views/basic-info/bas-station/basStationPage.helpers.js b/rsf-design/src/views/basic-info/bas-station/basStationPage.helpers.js
new file mode 100644
index 0000000..6750a27
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station/basStationPage.helpers.js
@@ -0,0 +1,531 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+const TYPE_META = {
+ 0: { text: '鏅鸿兘绔欑偣', type: 'primary' },
+ 1: { text: '鏅�氱珯鐐�', type: 'warning' }
+}
+
+const BOOLEAN_META = {
+ 1: { text: '鏄�', type: 'success' },
+ 0: { text: '鍚�', type: 'info' }
+}
+
+export const BAS_STATION_REPORT_TITLE = '绔欑偣绠$悊鎶ヨ〃'
+export const BAS_STATION_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeIdArray(values = []) {
+ if (!Array.isArray(values)) {
+ return []
+ }
+
+ return values
+ .map((item, index) => {
+ if (item === null || item === undefined || item === '') {
+ return null
+ }
+ if (typeof item === 'object') {
+ const id = normalizeNumber(item.id ?? item.areaId ?? item.value, void 0)
+ if (id === void 0) {
+ return null
+ }
+ return {
+ id,
+ sort: normalizeNumber(item.sort, index + 1)
+ }
+ }
+ const id = normalizeNumber(item, void 0)
+ if (id === void 0) {
+ return null
+ }
+ return {
+ id,
+ sort: index + 1
+ }
+ })
+ .filter(Boolean)
+ .sort((left, right) => {
+ const leftSort = normalizeNumber(left.sort, Number.MAX_SAFE_INTEGER)
+ const rightSort = normalizeNumber(right.sort, Number.MAX_SAFE_INTEGER)
+ return leftSort - rightSort
+ })
+ .map((item, index) => ({
+ id: item.id,
+ sort: index + 1
+ }))
+}
+
+function normalizePlainIdArray(values = []) {
+ if (!Array.isArray(values)) {
+ return []
+ }
+ return values
+ .map((item) => normalizeNumber(item, void 0))
+ .filter((item) => item !== void 0 && item !== null)
+}
+
+export function createBasStationSearchState() {
+ return {
+ condition: '',
+ stationName: '',
+ stationId: '',
+ type: '',
+ useStatus: '',
+ area: '',
+ isCrossZone: '',
+ isWcs: '',
+ barcode: '',
+ autoTransfer: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createBasStationFormState() {
+ return {
+ id: void 0,
+ stationName: '',
+ stationId: '',
+ type: 0,
+ area: void 0,
+ useStatus: '',
+ isCrossZone: 0,
+ areaIds: [],
+ isWcs: 0,
+ wcsData: '',
+ containerTypes: [],
+ barcode: '',
+ autoTransfer: 0,
+ inAble: 0,
+ outAble: 0,
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getBasStationPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getBasStationStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getBasStationTypeOptions() {
+ return [
+ { label: '鏅鸿兘绔欑偣', value: 0 },
+ { label: '鏅�氱珯鐐�', value: 1 }
+ ]
+}
+
+export function getBasStationBooleanOptions() {
+ return [
+ { label: '鍚�', value: 0 },
+ { label: '鏄�', value: 1 }
+ ]
+}
+
+export function getBasStationUseStatusMeta(value) {
+ if (!value) {
+ return { text: '鏈煡', type: 'info' }
+ }
+ return {
+ text: normalizeText(value),
+ type: 'primary'
+ }
+}
+
+export function getBasStationStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function getBasStationTypeMeta(type) {
+ if (type === true || Number(type) === 0) {
+ return TYPE_META[0]
+ }
+ if (Number(type) === 1) {
+ return TYPE_META[1]
+ }
+ return { text: '鏈煡', type: 'info' }
+}
+
+export function getBasStationBooleanMeta(value) {
+ if (value === true || Number(value) === 1) {
+ return BOOLEAN_META[1]
+ }
+ if (value === false || Number(value) === 0) {
+ return BOOLEAN_META[0]
+ }
+ return { text: '鏈煡', type: 'info' }
+}
+
+export function buildBasStationSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ stationName: normalizeText(params.stationName),
+ stationId: normalizeText(params.stationId),
+ type:
+ params.type !== undefined && params.type !== null && params.type !== ''
+ ? Number(params.type)
+ : void 0,
+ useStatus: normalizeText(params.useStatus),
+ area:
+ params.area !== undefined && params.area !== null && params.area !== ''
+ ? Number(params.area)
+ : void 0,
+ isCrossZone:
+ params.isCrossZone !== undefined && params.isCrossZone !== null && params.isCrossZone !== ''
+ ? Number(params.isCrossZone)
+ : void 0,
+ isWcs:
+ params.isWcs !== undefined && params.isWcs !== null && params.isWcs !== ''
+ ? Number(params.isWcs)
+ : void 0,
+ barcode: normalizeText(params.barcode),
+ autoTransfer:
+ params.autoTransfer !== undefined && params.autoTransfer !== null && params.autoTransfer !== ''
+ ? Number(params.autoTransfer)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildBasStationPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildBasStationSearchParams(params)
+ }
+}
+
+export function buildBasStationSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ stationName: normalizeText(formData.stationName) || '',
+ stationId: normalizeText(formData.stationId) || '',
+ ...(formData.type !== void 0 && formData.type !== null && formData.type !== ''
+ ? { type: Number(formData.type) }
+ : {}),
+ ...(formData.area !== void 0 && formData.area !== null && formData.area !== ''
+ ? { area: Number(formData.area) }
+ : {}),
+ useStatus: normalizeText(formData.useStatus) || '',
+ ...(formData.isCrossZone !== void 0 && formData.isCrossZone !== null && formData.isCrossZone !== ''
+ ? { isCrossZone: Number(formData.isCrossZone) }
+ : {}),
+ ...(normalizeIdArray(formData.areaIds).length ? { areaIds: normalizeIdArray(formData.areaIds).map((item) => item.id) } : {}),
+ ...(formData.isWcs !== void 0 && formData.isWcs !== null && formData.isWcs !== ''
+ ? { isWcs: Number(formData.isWcs) }
+ : {}),
+ wcsData: normalizeText(formData.wcsData) || '',
+ ...(normalizePlainIdArray(formData.containerTypes).length
+ ? { containerTypes: normalizePlainIdArray(formData.containerTypes) }
+ : {}),
+ barcode: normalizeText(formData.barcode) || '',
+ ...(formData.autoTransfer !== void 0 && formData.autoTransfer !== null && formData.autoTransfer !== ''
+ ? { autoTransfer: Number(formData.autoTransfer) }
+ : {}),
+ ...(formData.inAble !== void 0 && formData.inAble !== null && formData.inAble !== ''
+ ? { inAble: Number(formData.inAble) }
+ : {}),
+ ...(formData.outAble !== void 0 && formData.outAble !== null && formData.outAble !== ''
+ ? { outAble: Number(formData.outAble) }
+ : {}),
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function createBasStationDialogModel(record = {}) {
+ return {
+ ...createBasStationFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ stationName: normalizeText(record.stationName || ''),
+ stationId: normalizeText(record.stationId || ''),
+ type:
+ record.type !== void 0 && record.type !== null && record.type !== ''
+ ? Number(record.type)
+ : 0,
+ area:
+ record.area !== void 0 && record.area !== null && record.area !== ''
+ ? Number(record.area)
+ : void 0,
+ useStatus: normalizeText(record.useStatus || record.useStatus$ || ''),
+ isCrossZone:
+ record.isCrossZone !== void 0 && record.isCrossZone !== null && record.isCrossZone !== ''
+ ? Number(record.isCrossZone)
+ : 0,
+ areaIds: normalizeIdArray(record.areaIds ?? record.crossZoneArea ?? []),
+ isWcs:
+ record.isWcs !== void 0 && record.isWcs !== null && record.isWcs !== ''
+ ? Number(record.isWcs)
+ : 0,
+ wcsData: normalizeText(record.wcsData || ''),
+ containerTypes: normalizePlainIdArray(record.containerTypes ?? record.containerType ?? record.containerTypes$ ?? []),
+ barcode: normalizeText(record.barcode || ''),
+ autoTransfer:
+ record.autoTransfer !== void 0 && record.autoTransfer !== null && record.autoTransfer !== ''
+ ? Number(record.autoTransfer)
+ : 0,
+ inAble: record.inAble !== void 0 && record.inAble !== null && record.inAble !== '' ? Number(record.inAble) : 0,
+ outAble:
+ record.outAble !== void 0 && record.outAble !== null && record.outAble !== ''
+ ? Number(record.outAble)
+ : 0,
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export const buildBasStationDialogModel = createBasStationDialogModel
+
+export function resolveBasStationAreaOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.areaId ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.name || item.areaName || item.code || item.areaCode || `搴撳尯 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function buildBasStationContainerTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.label || item.name || item.dictLabel || item.value || '')
+ }
+ })
+ .filter(Boolean)
+}
+
+export function buildBasStationUseStatusOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: normalizeText(value),
+ label: normalizeText(item.label || item.name || item.dictLabel || item.value || '')
+ }
+ })
+ .filter(Boolean)
+}
+
+function buildIdLabelText(records = [], resolveLabel, fallbackPrefix) {
+ if (!Array.isArray(records) || !records.length) {
+ return ''
+ }
+ const labels = records
+ .map((item) => {
+ if (item === null || item === undefined || item === '') {
+ return ''
+ }
+ const id = typeof item === 'object' ? item.id ?? item.areaId ?? item.value : item
+ if (typeof resolveLabel === 'function') {
+ const resolvedLabel = normalizeText(resolveLabel(id))
+ if (resolvedLabel) {
+ return resolvedLabel
+ }
+ }
+ if (typeof item === 'object') {
+ return normalizeText(item.name || item.areaName || item.label || item.code || item.areaCode || id)
+ }
+ return normalizeText(`${fallbackPrefix} ${id}`)
+ })
+ .filter(Boolean)
+ return labels.join('銆�')
+}
+
+function buildArrayText(records = [], resolveLabel, fallbackPrefix) {
+ if (!Array.isArray(records) || !records.length) {
+ return ''
+ }
+ return records
+ .map((item) => {
+ const id = typeof item === 'object' ? item.id ?? item.value : item
+ if (typeof resolveLabel === 'function') {
+ const resolved = normalizeText(resolveLabel(id))
+ if (resolved) {
+ return resolved
+ }
+ }
+ if (typeof item === 'object') {
+ return normalizeText(item.label || item.name || item.dictLabel || item.value || id)
+ }
+ return normalizeText(`${fallbackPrefix} ${id}`)
+ })
+ .filter(Boolean)
+ .join('銆�')
+}
+
+export function normalizeBasStationDetailRecord(record = {}, resolveAreaLabel, resolveContainerTypeLabel) {
+ const areaIds = normalizeIdArray(record.areaIds ?? record.crossZoneArea ?? [])
+ const containerTypes = normalizePlainIdArray(record.containerTypes ?? record.containerType ?? record.containerTypes$ ?? [])
+ const statusMeta = getBasStationStatusMeta(record.statusBool ?? record.status)
+ const typeMeta = getBasStationTypeMeta(record.type)
+ const useStatusMeta = getBasStationUseStatusMeta(record.useStatus$ || record.useStatus)
+
+ return {
+ ...record,
+ stationName: normalizeText(record.stationName || ''),
+ stationId: normalizeText(record.stationId || ''),
+ type: record.type !== void 0 && record.type !== null && record.type !== '' ? Number(record.type) : void 0,
+ typeText: typeMeta.text,
+ useStatus: normalizeText(record.useStatus || record.useStatus$ || ''),
+ useStatusText: useStatusMeta.text,
+ area: record.area !== void 0 && record.area !== null && record.area !== '' ? Number(record.area) : void 0,
+ areaText: normalizeText(resolveAreaLabel?.(record.area) || record.area$ || record.areaName || ''),
+ areaIds,
+ crossZoneAreaText: buildIdLabelText(areaIds, resolveAreaLabel, '搴撳尯'),
+ isCrossZone: record.isCrossZone !== void 0 && record.isCrossZone !== null && record.isCrossZone !== ''
+ ? Number(record.isCrossZone)
+ : void 0,
+ isCrossZoneText: getBasStationBooleanMeta(record.isCrossZone).text,
+ isWcs: record.isWcs !== void 0 && record.isWcs !== null && record.isWcs !== ''
+ ? Number(record.isWcs)
+ : void 0,
+ isWcsText: getBasStationBooleanMeta(record.isWcs).text,
+ wcsData: normalizeText(record.wcsData || ''),
+ containerTypes,
+ containerTypesText: buildArrayText(containerTypes, resolveContainerTypeLabel, '瀹瑰櫒绫诲瀷'),
+ barcode: normalizeText(record.barcode || ''),
+ autoTransfer: record.autoTransfer !== void 0 && record.autoTransfer !== null && record.autoTransfer !== ''
+ ? Number(record.autoTransfer)
+ : void 0,
+ autoTransferText: getBasStationBooleanMeta(record.autoTransfer).text,
+ inAble: record.inAble !== void 0 && record.inAble !== null && record.inAble !== ''
+ ? Number(record.inAble)
+ : void 0,
+ inAbleText: getBasStationBooleanMeta(record.inAble).text,
+ outAble: record.outAble !== void 0 && record.outAble !== null && record.outAble !== ''
+ ? Number(record.outAble)
+ : void 0,
+ outAbleText: getBasStationBooleanMeta(record.outAble).text,
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ stationAlias: Array.isArray(record.stationAlias) ? [...record.stationAlias] : record.stationAlias,
+ stationAliasText: Array.isArray(record.stationAlias)
+ ? record.stationAlias.map((item) => normalizeText(item)).filter(Boolean).join('銆�')
+ : normalizeText(record.stationAlias || record.stationAlias$ || ''),
+ productionLineCode: normalizeText(record.productionLineCode || ''),
+ productionLineName: normalizeText(record.productionLineName || ''),
+ memo: normalizeText(record.memo || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeBasStationListRow(record = {}, resolveAreaLabel, resolveContainerTypeLabel) {
+ return normalizeBasStationDetailRecord(record, resolveAreaLabel, resolveContainerTypeLabel)
+}
+
+export function buildBasStationPrintRows(records = [], resolveAreaLabel, resolveContainerTypeLabel) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeBasStationListRow(record, resolveAreaLabel, resolveContainerTypeLabel))
+}
+
+export function buildBasStationReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = BAS_STATION_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: BAS_STATION_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...BAS_STATION_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/bas-station/basStationTable.columns.js b/rsf-design/src/views/basic-info/bas-station/basStationTable.columns.js
new file mode 100644
index 0000000..1fabdbc
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station/basStationTable.columns.js
@@ -0,0 +1,174 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import {
+ getBasStationUseStatusMeta,
+ getBasStationStatusMeta,
+ getBasStationTypeMeta
+} from './basStationPage.helpers'
+
+export function createBasStationTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'stationName',
+ label: '绔欑偣缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.stationName || '--'
+ },
+ {
+ prop: 'stationId',
+ label: '绔欑偣鍚嶇О',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.stationId || '--'
+ },
+ {
+ prop: 'typeText',
+ label: '绔欑偣绫诲瀷',
+ width: 110,
+ align: 'center',
+ formatter: (row) => {
+ const typeMeta = getBasStationTypeMeta(row.type)
+ return h(ElTag, { type: typeMeta.type, effect: 'light' }, () => typeMeta.text)
+ }
+ },
+ {
+ prop: 'useStatusText',
+ label: '浣跨敤鐘舵��',
+ width: 110,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getBasStationUseStatusMeta(row.useStatusText || row.useStatus || '')
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text || '--')
+ }
+ },
+ {
+ prop: 'areaText',
+ label: '鎵�灞炲簱鍖�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.areaText || row.area$ || '--'
+ },
+ {
+ prop: 'crossZoneAreaText',
+ label: '鍙法鍖哄簱鍖�',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.crossZoneAreaText || '--'
+ },
+ {
+ prop: 'containerTypesText',
+ label: '鍙叆瀹瑰櫒绫诲瀷',
+ minWidth: 200,
+ showOverflowTooltip: true,
+ formatter: (row) => row.containerTypesText || '--'
+ },
+ {
+ prop: 'barcode',
+ label: '鏉$爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.barcode || '--'
+ },
+ {
+ prop: 'inAbleText',
+ label: '鍙叆',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.inAbleText || '--'
+ },
+ {
+ prop: 'outAbleText',
+ label: '鍙嚭',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.outAbleText || '--'
+ },
+ {
+ prop: 'isCrossZoneText',
+ label: '鏄惁璺ㄥ尯',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.isCrossZoneText || '--'
+ },
+ {
+ prop: 'isWcsText',
+ label: '鏄惁WCS',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.isWcsText || '--'
+ },
+ {
+ prop: 'autoTransferText',
+ label: '鑷姩璋冩嫧',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.autoTransferText || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getBasStationStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || row.updateBy$ || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/bas-station/index.vue b/rsf-design/src/views/basic-info/bas-station/index.vue
new file mode 100644
index 0000000..a5dd00a
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station/index.vue
@@ -0,0 +1,472 @@
+<template>
+ <div class="bas-station-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板绔欑偣</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <BasStationDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :station-data="currentStationData"
+ :area-options="areaOptions"
+ :container-type-options="containerTypeOptions"
+ :use-status-options="useStatusOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <BasStationDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :resolve-area-label="resolveAreaLabel"
+ :resolve-container-type-label="resolveContainerTypeLabel"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
+ import {
+ fetchBasStationPage,
+ fetchDeleteBasStation,
+ fetchExportBasStationReport,
+ fetchGetBasStationDetail,
+ fetchGetBasStationMany,
+ fetchSaveBasStation,
+ fetchUpdateBasStation
+ } from '@/api/bas-station'
+ import BasStationDialog from './modules/bas-station-dialog.vue'
+ import BasStationDetailDrawer from './modules/bas-station-detail-drawer.vue'
+ import { createBasStationTableColumns } from './basStationTable.columns'
+ import {
+ BAS_STATION_REPORT_STYLE,
+ BAS_STATION_REPORT_TITLE,
+ buildBasStationDialogModel,
+ buildBasStationPageQueryParams,
+ buildBasStationPrintRows,
+ buildBasStationReportMeta,
+ buildBasStationSavePayload,
+ buildBasStationSearchParams,
+ buildBasStationContainerTypeOptions,
+ buildBasStationUseStatusOptions,
+ createBasStationSearchState,
+ getBasStationPaginationKey,
+ normalizeBasStationDetailRecord,
+ normalizeBasStationListRow,
+ getBasStationTypeOptions,
+ getBasStationBooleanOptions,
+ resolveBasStationAreaOptions
+ } from './basStationPage.helpers'
+
+ defineOptions({ name: 'BasStation' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createBasStationSearchState())
+ const areaOptions = ref([])
+ const containerTypeOptions = ref([])
+ const useStatusOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = BAS_STATION_REPORT_TITLE
+ const reportQueryParams = computed(() => buildBasStationSearchParams(searchForm.value))
+
+ const areaLabelMap = computed(
+ () =>
+ new Map(
+ areaOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+
+ const containerTypeLabelMap = computed(
+ () =>
+ new Map(
+ containerTypeOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+
+ function resolveAreaLabel(id) {
+ return areaLabelMap.value.get(String(id)) || ''
+ }
+
+ function resolveContainerTypeLabel(value) {
+ return containerTypeLabelMap.value.get(String(value)) || ''
+ }
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐圭紪鐮�/绔欑偣鍚嶇О/澶囨敞'
+ }
+ },
+ {
+ label: '绔欑偣缂栫爜',
+ key: 'stationName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐圭紪鐮�'
+ }
+ },
+ {
+ label: '绔欑偣鍚嶇О',
+ key: 'stationId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐瑰悕绉�'
+ }
+ },
+ {
+ label: '绔欑偣绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationTypeOptions()
+ }
+ },
+ {
+ label: '浣跨敤鐘舵��',
+ key: 'useStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: useStatusOptions.value
+ }
+ },
+ {
+ label: '鎵�灞炲簱鍖�',
+ key: 'area',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: areaOptions.value
+ }
+ },
+ {
+ label: '鏄惁璺ㄥ尯',
+ key: 'isCrossZone',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationBooleanOptions()
+ }
+ },
+ {
+ label: '鏄惁WCS',
+ key: 'isWcs',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationBooleanOptions()
+ }
+ },
+ {
+ label: '鏉$爜',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯鐮�'
+ }
+ },
+ {
+ label: '鑷姩璋冩嫧',
+ key: 'autoTransfer',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getBasStationBooleanOptions()
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨寮�濮嬫椂闂�'
+ }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨缁撴潫鏃堕棿'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetBasStationDetail(row.id), {}, {
+ timeoutMessage: '绔欑偣璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeBasStationDetailRecord(detail, resolveAreaLabel, resolveContainerTypeLabel)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇绔欑偣璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetBasStationDetail(row.id), {}, {
+ timeoutMessage: '绔欑偣璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇绔欑偣璇︽儏澶辫触')
+ }
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchBasStationPage,
+ apiParams: buildBasStationPageQueryParams(searchForm.value),
+ paginationKey: getBasStationPaginationKey(),
+ columnsFactory: () =>
+ createBasStationTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeBasStationListRow(item, resolveAreaLabel, resolveContainerTypeLabel))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentStationData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildBasStationDialogModel(),
+ buildEditModel: (record) => buildBasStationDialogModel(record),
+ buildSavePayload: (formData) => buildBasStationSavePayload(formData),
+ saveRequest: fetchSaveBasStation,
+ updateRequest: fetchUpdateBasStation,
+ deleteRequest: fetchDeleteBasStation,
+ entityName: '绔欑偣',
+ resolveRecordLabel: (record) => record?.stationName || record?.stationId || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...BAS_STATION_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetBasStationMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchBasStationPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'bas-station.xlsx',
+ requestExport: (payload) =>
+ fetchExportBasStationReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildBasStationPrintRows(records, resolveAreaLabel, resolveContainerTypeLabel),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildBasStationReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || BAS_STATION_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildBasStationSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createBasStationSearchState())
+ resetSearchParams()
+ }
+
+ async function loadAreaOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveBasStationAreaOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadContainerTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_container_type',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '瀹瑰櫒绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ containerTypeOptions.value = buildBasStationContainerTypeOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadUseStatusOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 100,
+ dictTypeCode: 'sys_sta_use_stas',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '浣跨敤鐘舵�佸姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ useStatusOptions.value = buildBasStationUseStatusOptions(defaultResponseAdapter(response).records)
+ }
+
+ onMounted(async () => {
+ await Promise.all([loadAreaOptions(), loadContainerTypeOptions(), loadUseStatusOptions()])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/bas-station/modules/bas-station-detail-drawer.vue b/rsf-design/src/views/basic-info/bas-station/modules/bas-station-detail-drawer.vue
new file mode 100644
index 0000000..d864f30
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station/modules/bas-station-detail-drawer.vue
@@ -0,0 +1,130 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="绔欑偣绠$悊璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="绔欑偣缂栫爜">{{ detail.stationName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣鍚嶇О">{{ detail.stationId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣绫诲瀷">
+ <ElTag :type="detail.typeType || 'info'" effect="light">
+ {{ detail.typeText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤鐘舵��">
+ <ElTag :type="detail.useStatusType || 'info'" effect="light">
+ {{ detail.useStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵�灞炲簱鍖�">{{ detail.areaText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉$爜">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙叆">{{ detail.inAbleText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙嚭">{{ detail.outAbleText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏄惁璺ㄥ尯">{{ detail.isCrossZoneText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏄惁WCS">{{ detail.isWcsText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑷姩璋冩嫧">{{ detail.autoTransferText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="涓氬姟閰嶇疆" :column="2" border>
+ <ElDescriptionsItem label="鍙法鍖哄簱鍖�" :span="2">
+ <div v-if="areaItems.length" class="flex flex-wrap gap-2">
+ <ElTag v-for="item in areaItems" :key="item.id" effect="plain">
+ #{{ item.sort }} {{ item.label }}
+ </ElTag>
+ </div>
+ <span v-else>--</span>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙叆瀹瑰櫒绫诲瀷" :span="2">
+ <div v-if="containerItems.length" class="flex flex-wrap gap-2">
+ <ElTag v-for="item in containerItems" :key="item.value" effect="plain">
+ {{ item.label }}
+ </ElTag>
+ </div>
+ <span v-else>--</span>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="WCS鏁版嵁" :span="2">{{ detail.wcsData || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣鍒悕" :span="2">{{ detail.stationAliasText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱缂栫爜">{{ detail.productionLineCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱鍚嶇О">{{ detail.productionLineName || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ resolveAreaLabel: { type: Function, default: null },
+ resolveContainerTypeLabel: { type: Function, default: null }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ const areaItems = computed(() => {
+ if (!Array.isArray(props.detail?.areaIds)) {
+ return []
+ }
+ return props.detail.areaIds
+ .map((item, index) => {
+ const id = typeof item === 'object' ? item.id ?? item.areaId ?? item.value : item
+ const label = typeof props.resolveAreaLabel === 'function' ? props.resolveAreaLabel(id) : ''
+ return {
+ id,
+ sort: typeof item === 'object' && item.sort !== undefined ? item.sort : index + 1,
+ label: String(label || item.name || item.areaName || item.label || item.code || item.areaCode || `搴撳尯 ${id}`)
+ }
+ })
+ .filter((item) => item.id !== undefined && item.id !== null && item.id !== '')
+ })
+
+ const containerItems = computed(() => {
+ if (!Array.isArray(props.detail?.containerTypes)) {
+ return []
+ }
+ return props.detail.containerTypes
+ .map((item) => {
+ const value = typeof item === 'object' ? item.id ?? item.value : item
+ const label = typeof props.resolveContainerTypeLabel === 'function' ? props.resolveContainerTypeLabel(value) : ''
+ return {
+ value,
+ label: String(label || item.label || item.name || item.dictLabel || item.value || `瀹瑰櫒绫诲瀷 ${value}`)
+ }
+ })
+ .filter((item) => item.value !== undefined && item.value !== null && item.value !== '')
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/bas-station/modules/bas-station-dialog.vue b/rsf-design/src/views/basic-info/bas-station/modules/bas-station-dialog.vue
new file mode 100644
index 0000000..b478cae
--- /dev/null
+++ b/rsf-design/src/views/basic-info/bas-station/modules/bas-station-dialog.vue
@@ -0,0 +1,292 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="980px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildBasStationDialogModel,
+ createBasStationFormState,
+ getBasStationBooleanOptions,
+ getBasStationStatusOptions,
+ getBasStationTypeOptions
+ } from '../basStationPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ stationData: { type: Object, default: () => ({}) },
+ areaOptions: { type: Array, default: () => [] },
+ containerTypeOptions: { type: Array, default: () => [] },
+ useStatusOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createBasStationFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫绔欑偣' : '鏂板绔欑偣'))
+
+ const rules = computed(() => ({
+ stationName: [{ required: true, message: '璇疯緭鍏ョ珯鐐圭紪鐮�', trigger: 'blur' }],
+ stationId: [{ required: true, message: '璇疯緭鍏ョ珯鐐瑰悕绉�', trigger: 'blur' }],
+ type: [{ required: true, message: '璇烽�夋嫨绔欑偣绫诲瀷', trigger: 'change' }],
+ area: [{ required: true, message: '璇烽�夋嫨鎵�灞炲簱鍖�', trigger: 'change' }],
+ useStatus: [{ required: true, message: '璇烽�夋嫨浣跨敤鐘舵��', trigger: 'change' }],
+ areaIds: [{ type: 'array', required: true, message: '璇烽�夋嫨鍙法鍖哄簱鍖�', trigger: 'change' }],
+ containerTypes: [{ type: 'array', required: true, message: '璇烽�夋嫨鍙叆瀹瑰櫒绫诲瀷', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '绔欑偣缂栫爜',
+ key: 'stationName',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ珯鐐圭紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '绔欑偣鍚嶇О',
+ key: 'stationId',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ珯鐐瑰悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '绔欑偣绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨绔欑偣绫诲瀷',
+ clearable: true,
+ options: getBasStationTypeOptions()
+ }
+ },
+ {
+ label: '鎵�灞炲簱鍖�',
+ key: 'area',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鎵�灞炲簱鍖�',
+ clearable: true,
+ filterable: true,
+ options: props.areaOptions || []
+ }
+ },
+ {
+ label: '浣跨敤鐘舵��',
+ key: 'useStatus',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨浣跨敤鐘舵��',
+ clearable: true,
+ filterable: true,
+ options: props.useStatusOptions || []
+ }
+ },
+ {
+ label: '鍙法鍖哄簱鍖�',
+ key: 'areaIds',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨鍙法鍖哄簱鍖�',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.areaOptions || []
+ }
+ },
+ {
+ label: '鍙叆瀹瑰櫒绫诲瀷',
+ key: 'containerTypes',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨鍙叆瀹瑰櫒绫诲瀷',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.containerTypeOptions || []
+ }
+ },
+ {
+ label: '鏄惁璺ㄥ尯',
+ key: 'isCrossZone',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏄惁璺ㄥ尯',
+ clearable: true,
+ options: getBasStationBooleanOptions()
+ }
+ },
+ {
+ label: '鏄惁WCS',
+ key: 'isWcs',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏄惁WCS',
+ clearable: true,
+ options: getBasStationBooleanOptions()
+ }
+ },
+ {
+ label: '鑷姩璋冩嫧',
+ key: 'autoTransfer',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鑷姩璋冩嫧',
+ clearable: true,
+ options: getBasStationBooleanOptions()
+ }
+ },
+ {
+ label: '鍙叆',
+ key: 'inAble',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鍙叆',
+ clearable: true,
+ options: getBasStationBooleanOptions()
+ }
+ },
+ {
+ label: '鍙嚭',
+ key: 'outAble',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鍙嚭',
+ clearable: true,
+ options: getBasStationBooleanOptions()
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getBasStationStatusOptions()
+ }
+ },
+ {
+ label: '鏉$爜',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ潯鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: 'WCS鏁版嵁',
+ key: 'wcsData',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏CS鏁版嵁',
+ clearable: true
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildBasStationDialogModel(props.stationData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createBasStationFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.stationData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/companys/companysPage.helpers.js b/rsf-design/src/views/basic-info/companys/companysPage.helpers.js
new file mode 100644
index 0000000..e375f44
--- /dev/null
+++ b/rsf-design/src/views/basic-info/companys/companysPage.helpers.js
@@ -0,0 +1,252 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const COMPANYS_REPORT_TITLE = '寰�鏉ヤ紒涓氭姤琛�'
+export const COMPANYS_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeString(value, fallback = '') {
+ const text = normalizeText(value)
+ return text || fallback
+}
+
+export function createCompanysSearchState() {
+ return {
+ condition: '',
+ code: '',
+ name: '',
+ nameEn: '',
+ breifCode: '',
+ type: '',
+ contact: '',
+ tel: '',
+ email: '',
+ pcode: '',
+ province: '',
+ city: '',
+ address: '',
+ status: '',
+ memo: ''
+ }
+}
+
+export function createCompanysFormState() {
+ return {
+ id: void 0,
+ code: '',
+ name: '',
+ nameEn: '',
+ breifCode: '',
+ type: '',
+ contact: '',
+ tel: '',
+ email: '',
+ pcode: '',
+ province: '',
+ city: '',
+ address: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getCompanysStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getCompanysPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getCompanysStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildCompanysTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: String(value),
+ label: normalizeText(item.label || item.name || item.dictLabel || item.value || '')
+ }
+ })
+ .filter(Boolean)
+}
+
+export function buildCompanysSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ name: normalizeText(params.name),
+ nameEn: normalizeText(params.nameEn),
+ breifCode: normalizeText(params.breifCode),
+ type: normalizeText(params.type),
+ contact: normalizeText(params.contact),
+ tel: normalizeText(params.tel),
+ email: normalizeText(params.email),
+ pcode: normalizeText(params.pcode),
+ province: normalizeText(params.province),
+ city: normalizeText(params.city),
+ address: normalizeText(params.address),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildCompanysPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildCompanysSearchParams(params)
+ }
+}
+
+export function buildCompanysSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ code: normalizeText(formData.code),
+ name: normalizeText(formData.name),
+ nameEn: normalizeText(formData.nameEn),
+ breifCode: normalizeText(formData.breifCode),
+ type: normalizeString(formData.type),
+ contact: normalizeText(formData.contact),
+ tel: normalizeText(formData.tel),
+ email: normalizeText(formData.email),
+ pcode: normalizeText(formData.pcode),
+ province: normalizeText(formData.province),
+ city: normalizeText(formData.city),
+ address: normalizeText(formData.address),
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo)
+ }
+}
+
+export function buildCompanysDialogModel(record = {}) {
+ return {
+ ...createCompanysFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ code: normalizeText(record.code || ''),
+ name: normalizeText(record.name || ''),
+ nameEn: normalizeText(record.nameEn || ''),
+ breifCode: normalizeText(record.breifCode || ''),
+ type: normalizeString(record.type || ''),
+ contact: normalizeText(record.contact || ''),
+ tel: normalizeText(record.tel || ''),
+ email: normalizeText(record.email || ''),
+ pcode: normalizeText(record.pcode || ''),
+ province: normalizeText(record.province || ''),
+ city: normalizeText(record.city || ''),
+ address: normalizeText(record.address || ''),
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeCompanysDetailRecord(record = {}, resolveTypeLabel) {
+ const statusMeta = getCompanysStatusMeta(record.statusBool ?? record.status)
+ const typeValue = record.type ?? ''
+ const typeText =
+ normalizeString(record.companys$ || record.type$ || record.typeText || '') ||
+ normalizeString(typeof resolveTypeLabel === 'function' ? resolveTypeLabel(typeValue) : '') ||
+ normalizeString(typeValue, '--')
+
+ return {
+ ...record,
+ code: normalizeText(record.code || ''),
+ name: normalizeText(record.name || ''),
+ nameEn: normalizeText(record.nameEn || ''),
+ breifCode: normalizeText(record.breifCode || ''),
+ type: normalizeString(typeValue),
+ typeText,
+ contact: normalizeText(record.contact || ''),
+ tel: normalizeText(record.tel || ''),
+ email: normalizeText(record.email || ''),
+ pcode: normalizeText(record.pcode || ''),
+ province: normalizeText(record.province || ''),
+ city: normalizeText(record.city || ''),
+ address: normalizeText(record.address || ''),
+ memo: normalizeText(record.memo || ''),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeCompanysListRow(record = {}, resolveTypeLabel) {
+ return normalizeCompanysDetailRecord(record, resolveTypeLabel)
+}
+
+export function buildCompanysPrintRows(records = [], resolveTypeLabel) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeCompanysListRow(record, resolveTypeLabel))
+}
+
+export function buildCompanysReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = COMPANYS_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: COMPANYS_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...COMPANYS_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/companys/companysTable.columns.js b/rsf-design/src/views/basic-info/companys/companysTable.columns.js
new file mode 100644
index 0000000..042186a
--- /dev/null
+++ b/rsf-design/src/views/basic-info/companys/companysTable.columns.js
@@ -0,0 +1,102 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getCompanysStatusMeta } from './companysPage.helpers'
+
+export function createCompanysTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true,
+ resolveTypeLabel
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'code', label: '浼佷笟缂栫爜', minWidth: 140, showOverflowTooltip: true, formatter: (row) => row.code || '--' },
+ { prop: 'name', label: '浼佷笟鍚嶇О', minWidth: 170, showOverflowTooltip: true, formatter: (row) => row.name || '--' },
+ { prop: 'nameEn', label: '鑻辨枃鍒悕', minWidth: 160, showOverflowTooltip: true, formatter: (row) => row.nameEn || '--' },
+ { prop: 'breifCode', label: '鍔╄鐮�', minWidth: 120, showOverflowTooltip: true, formatter: (row) => row.breifCode || '--' },
+ {
+ prop: 'typeText',
+ label: '浼佷笟绫诲瀷',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) =>
+ row.typeText || (typeof resolveTypeLabel === 'function' ? resolveTypeLabel(row.type) : '') || '--'
+ },
+ { prop: 'contact', label: '鑱旂郴浜�', minWidth: 120, showOverflowTooltip: true, formatter: (row) => row.contact || '--' },
+ { prop: 'tel', label: '鑱旂郴鐢佃瘽', minWidth: 140, showOverflowTooltip: true, formatter: (row) => row.tel || '--' },
+ { prop: 'email', label: '閭', minWidth: 180, showOverflowTooltip: true, formatter: (row) => row.email || '--' },
+ { prop: 'pcode', label: '閭紪', minWidth: 120, showOverflowTooltip: true, formatter: (row) => row.pcode || '--' },
+ { prop: 'province', label: '鐪佷唤', minWidth: 120, showOverflowTooltip: true, formatter: (row) => row.province || '--' },
+ { prop: 'city', label: '鍩庡競', minWidth: 120, showOverflowTooltip: true, formatter: (row) => row.city || '--' },
+ { prop: 'address', label: '鍦板潃', minWidth: 220, showOverflowTooltip: true, formatter: (row) => row.address || '--' },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getCompanysStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ { prop: 'memo', label: '澶囨敞', minWidth: 180, showOverflowTooltip: true, formatter: (row) => row.memo || '--' },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || row.createBy$ || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || row.createTime$ || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || row.updateBy$ || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/companys/index.vue b/rsf-design/src/views/basic-info/companys/index.vue
new file mode 100644
index 0000000..abc0bf2
--- /dev/null
+++ b/rsf-design/src/views/basic-info/companys/index.vue
@@ -0,0 +1,355 @@
+<template>
+ <div class="companys-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板浼佷笟</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <CompanysDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :companys-data="currentCompanysData"
+ :type-options="typeOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <CompanysDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import {
+ fetchCompanysPage,
+ fetchDeleteCompanys,
+ fetchExportCompanysReport,
+ fetchGetCompanysDetail,
+ fetchGetCompanysMany,
+ fetchSaveCompanys,
+ fetchUpdateCompanys
+ } from '@/api/companys'
+ import CompanysDialog from './modules/companys-dialog.vue'
+ import CompanysDetailDrawer from './modules/companys-detail-drawer.vue'
+ import { createCompanysTableColumns } from './companysTable.columns'
+ import {
+ COMPANYS_REPORT_STYLE,
+ COMPANYS_REPORT_TITLE,
+ buildCompanysDialogModel,
+ buildCompanysPageQueryParams,
+ buildCompanysSavePayload,
+ buildCompanysPrintRows,
+ buildCompanysReportMeta,
+ buildCompanysSearchParams,
+ buildCompanysTypeOptions,
+ createCompanysSearchState,
+ getCompanysPaginationKey,
+ normalizeCompanysListRow
+ } from './companysPage.helpers'
+
+ defineOptions({ name: 'Companys' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createCompanysSearchState())
+ const typeOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = COMPANYS_REPORT_TITLE
+ const reportQueryParams = computed(() => buildCompanysSearchParams(searchForm.value))
+ const typeLabelMap = computed(
+ () =>
+ new Map(
+ typeOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+
+ function resolveTypeLabel(value) {
+ return typeLabelMap.value.get(String(value)) || ''
+ }
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ紒涓氬悕绉�/缂栫爜/鑱旂郴浜�/鐢佃瘽'
+ }
+ },
+ { label: '浼佷笟缂栫爜', key: 'code', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヤ紒涓氱紪鐮�' } },
+ { label: '浼佷笟鍚嶇О', key: 'name', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヤ紒涓氬悕绉�' } },
+ { label: '鑻辨枃鍒悕', key: 'nameEn', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ嫳鏂囧埆鍚�' } },
+ { label: '鍔╄鐮�', key: 'breifCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ姪璁扮爜' } },
+ {
+ label: '浼佷笟绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: typeOptions.value
+ }
+ },
+ { label: '鑱旂郴浜�', key: 'contact', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ仈绯讳汉' } },
+ { label: '鑱旂郴鐢佃瘽', key: 'tel', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ仈绯荤數璇�' } },
+ { label: '閭', key: 'email', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ラ偖绠�' } },
+ { label: '閭紪', key: 'pcode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ラ偖缂�' } },
+ { label: '鐪佷唤', key: 'province', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ渷浠�' } },
+ { label: '鍩庡競', key: 'city', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ煄甯�' } },
+ { label: '鍦板潃', key: 'address', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ湴鍧�' } },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ { label: '澶囨敞', key: 'memo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' } }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetCompanysDetail(row.id), {}, {
+ timeoutMessage: '寰�鏉ヤ紒涓氳鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ detailData.value = normalizeCompanysListRow(detail, resolveTypeLabel)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇寰�鏉ヤ紒涓氳鎯呭け璐�')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetCompanysDetail(row.id), {}, {
+ timeoutMessage: '寰�鏉ヤ紒涓氳鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇寰�鏉ヤ紒涓氳鎯呭け璐�')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchCompanysPage,
+ apiParams: buildCompanysPageQueryParams(searchForm.value),
+ paginationKey: getCompanysPaginationKey(),
+ columnsFactory: () =>
+ createCompanysTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ resolveTypeLabel,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeCompanysListRow(item, resolveTypeLabel))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentCompanysData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildCompanysDialogModel(),
+ buildEditModel: (record) => buildCompanysDialogModel(record),
+ buildSavePayload: (formData) => buildCompanysSavePayload(formData),
+ saveRequest: fetchSaveCompanys,
+ updateRequest: fetchUpdateCompanys,
+ deleteRequest: fetchDeleteCompanys,
+ entityName: '寰�鏉ヤ紒涓�',
+ resolveRecordLabel: (record) => record?.name || record?.code || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...COMPANYS_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetCompanysMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchCompanysPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'companys.xlsx',
+ requestExport: (payload) =>
+ fetchExportCompanysReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildCompanysPrintRows(records, resolveTypeLabel),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildCompanysReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || COMPANYS_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildCompanysSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createCompanysSearchState())
+ resetSearchParams()
+ }
+
+ async function loadTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_companys_type',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '浼佷笟绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ typeOptions.value = buildCompanysTypeOptions(defaultResponseAdapter(response).records)
+ }
+
+ onMounted(async () => {
+ await loadTypeOptions()
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/companys/modules/companys-detail-drawer.vue b/rsf-design/src/views/basic-info/companys/modules/companys-detail-drawer.vue
new file mode 100644
index 0000000..3c6bb18
--- /dev/null
+++ b/rsf-design/src/views/basic-info/companys/modules/companys-detail-drawer.vue
@@ -0,0 +1,65 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="寰�鏉ヤ紒涓氳鎯�"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="浼佷笟缂栫爜">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浼佷笟鍚嶇О">{{ detail.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑻辨枃鍒悕">{{ detail.nameEn || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍔╄鐮�">{{ detail.breifCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浼佷笟绫诲瀷">{{ detail.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑱旂郴浜�">{{ detail.contact || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑱旂郴鐢佃瘽">{{ detail.tel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閭">{{ detail.email || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閭紪">{{ detail.pcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐪佷唤">{{ detail.province || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍩庡競">{{ detail.city || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍦板潃" :span="2">{{ detail.address || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/companys/modules/companys-dialog.vue b/rsf-design/src/views/basic-info/companys/modules/companys-dialog.vue
new file mode 100644
index 0000000..02459c0
--- /dev/null
+++ b/rsf-design/src/views/basic-info/companys/modules/companys-dialog.vue
@@ -0,0 +1,247 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="960px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildCompanysDialogModel,
+ createCompanysFormState,
+ getCompanysStatusOptions
+ } from '../companysPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ companysData: { type: Object, default: () => ({}) },
+ typeOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createCompanysFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫寰�鏉ヤ紒涓�' : '鏂板寰�鏉ヤ紒涓�'))
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ヤ紒涓氬悕绉�', trigger: 'blur' }],
+ breifCode: [{ required: true, message: '璇疯緭鍏ュ姪璁扮爜', trigger: 'blur' }],
+ type: [{ required: true, message: '璇烽�夋嫨浼佷笟绫诲瀷', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '浼佷笟缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '鐣欑┖灏嗚嚜鍔ㄧ敓鎴�',
+ clearable: true,
+ disabled: isEdit.value
+ }
+ },
+ {
+ label: '浼佷笟鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヤ紒涓氬悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '鑻辨枃鍒悕',
+ key: 'nameEn',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヨ嫳鏂囧埆鍚�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍔╄鐮�',
+ key: 'breifCode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ姪璁扮爜',
+ clearable: true
+ }
+ },
+ {
+ label: '浼佷笟绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨浼佷笟绫诲瀷',
+ clearable: true,
+ filterable: true,
+ options: props.typeOptions
+ }
+ },
+ {
+ label: '鑱旂郴浜�',
+ key: 'contact',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヨ仈绯讳汉',
+ clearable: true
+ }
+ },
+ {
+ label: '鑱旂郴鐢佃瘽',
+ key: 'tel',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヨ仈绯荤數璇�',
+ clearable: true
+ }
+ },
+ {
+ label: '閭',
+ key: 'email',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ偖绠�',
+ clearable: true
+ }
+ },
+ {
+ label: '閭紪',
+ key: 'pcode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ偖缂�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐪佷唤',
+ key: 'province',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ渷浠�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍩庡競',
+ key: 'city',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ煄甯�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍦板潃',
+ key: 'address',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ュ湴鍧�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getCompanysStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildCompanysDialogModel(props.companysData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createCompanysFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.companysData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/contract/contractPage.helpers.js b/rsf-design/src/views/basic-info/contract/contractPage.helpers.js
new file mode 100644
index 0000000..77df180
--- /dev/null
+++ b/rsf-design/src/views/basic-info/contract/contractPage.helpers.js
@@ -0,0 +1,162 @@
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+export const CONTRACT_REPORT_TITLE = '鍚堝悓淇℃伅鎶ヨ〃'
+export const CONTRACT_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export function createContractSearchState() {
+ return {
+ condition: '',
+ code: '',
+ name: '',
+ projectName: '',
+ status: '',
+ memo: ''
+ }
+}
+
+export function createContractFormState() {
+ return {
+ id: void 0,
+ code: '',
+ name: '',
+ projectName: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getContractPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getContractStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getContractStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildContractSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'name', 'projectName', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.status !== '' && params.status !== null && params.status !== undefined) {
+ result.status = Number(params.status)
+ }
+
+ return result
+}
+
+export function buildContractPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildContractSearchParams(params)
+ }
+}
+
+export function buildContractSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ code: normalizeText(formData.code),
+ name: normalizeText(formData.name),
+ projectName: normalizeText(formData.projectName),
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo)
+ }
+}
+
+export function buildContractDialogModel(record = {}) {
+ return {
+ ...createContractFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ code: normalizeText(record.code || ''),
+ name: normalizeText(record.name || ''),
+ projectName: normalizeText(record.projectName || ''),
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeContractListRow(record = {}) {
+ const statusMeta = getContractStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ code: normalizeText(record.code) || '--',
+ name: normalizeText(record.name) || '--',
+ projectName: normalizeText(record.projectName) || '--',
+ memo: normalizeText(record.memo) || '--',
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record.createBy$ || record.createBy || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateBy || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeContractDetailRecord(record = {}) {
+ return normalizeContractListRow(record)
+}
+
+export function buildContractPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeContractListRow(record))
+}
+
+export function buildContractReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = CONTRACT_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: CONTRACT_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...CONTRACT_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/contract/contractTable.columns.js b/rsf-design/src/views/basic-info/contract/contractTable.columns.js
new file mode 100644
index 0000000..3e2db3d
--- /dev/null
+++ b/rsf-design/src/views/basic-info/contract/contractTable.columns.js
@@ -0,0 +1,99 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+
+export function createContractTableColumns({ handleView, handleEdit, handleDelete, canEdit = true, canDelete = true }) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'code',
+ label: '鍚堝悓缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'name',
+ label: '鍚堝悓鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'projectName',
+ label: '椤圭洰鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '--')
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 200,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/contract/index.vue b/rsf-design/src/views/basic-info/contract/index.vue
new file mode 100644
index 0000000..cfc778e
--- /dev/null
+++ b/rsf-design/src/views/basic-info/contract/index.vue
@@ -0,0 +1,316 @@
+<template>
+ <div class="contract-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鍚堝悓淇℃伅</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <ContractDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :contract-data="currentContractData"
+ @submit="handleDialogSubmit"
+ />
+
+ <ContractDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchContractPage,
+ fetchDeleteContract,
+ fetchExportContractReport,
+ fetchGetContractDetail,
+ fetchGetContractMany,
+ fetchSaveContract,
+ fetchUpdateContract
+ } from '@/api/contract'
+ import ContractDialog from './modules/contract-dialog.vue'
+ import ContractDetailDrawer from './modules/contract-detail-drawer.vue'
+ import { createContractTableColumns } from './contractTable.columns'
+ import {
+ buildContractDialogModel,
+ buildContractPageQueryParams,
+ buildContractPrintRows,
+ buildContractReportMeta,
+ buildContractSavePayload,
+ buildContractSearchParams,
+ createContractFormState,
+ createContractSearchState,
+ CONTRACT_REPORT_STYLE,
+ CONTRACT_REPORT_TITLE,
+ getContractPaginationKey,
+ normalizeContractDetailRecord,
+ normalizeContractListRow
+ } from './contractPage.helpers'
+
+ defineOptions({ name: 'Contract' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createContractSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = CONTRACT_REPORT_TITLE
+ const reportQueryParams = computed(() => buildContractSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悎鍚岀紪鐮�/鍚嶇О/椤圭洰鍚嶇О/澶囨敞'
+ }
+ },
+ {
+ label: '鍚堝悓缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悎鍚岀紪鐮�'
+ }
+ },
+ {
+ label: '鍚堝悓鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悎鍚屽悕绉�'
+ }
+ },
+ {
+ label: '椤圭洰鍚嶇О',
+ key: 'projectName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ」鐩悕绉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetContractDetail(row.id), {}, {
+ timeoutMessage: '鍚堝悓淇℃伅璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeContractDetailRecord(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鍚堝悓淇℃伅璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetContractDetail(row.id), {}, {
+ timeoutMessage: '鍚堝悓淇℃伅璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇鍚堝悓淇℃伅璇︽儏澶辫触')
+ }
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchContractPage,
+ apiParams: buildContractPageQueryParams(searchForm.value),
+ paginationKey: getContractPaginationKey(),
+ columnsFactory: () =>
+ createContractTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeContractListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentContractData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildContractDialogModel(createContractFormState()),
+ buildEditModel: (record) => buildContractDialogModel(record),
+ buildSavePayload: (formData) => buildContractSavePayload(formData),
+ saveRequest: fetchSaveContract,
+ updateRequest: fetchUpdateContract,
+ deleteRequest: fetchDeleteContract,
+ entityName: '鍚堝悓淇℃伅',
+ resolveRecordLabel: (record) => record?.code || record?.name || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...CONTRACT_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetContractMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchContractPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'contract.xlsx',
+ requestExport: (payload) =>
+ fetchExportContractReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildContractPrintRows(records),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildContractReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || CONTRACT_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildContractSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createContractSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/contract/modules/contract-detail-drawer.vue b/rsf-design/src/views/basic-info/contract/modules/contract-detail-drawer.vue
new file mode 100644
index 0000000..1df6729
--- /dev/null
+++ b/rsf-design/src/views/basic-info/contract/modules/contract-detail-drawer.vue
@@ -0,0 +1,55 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鍚堝悓淇℃伅璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="10" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="ID">{{ detail.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚堝悓缂栫爜">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚堝悓鍚嶇О">{{ detail.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰鍚嶇О">{{ detail.projectName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ContractDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/contract/modules/contract-dialog.vue b/rsf-design/src/views/basic-info/contract/modules/contract-dialog.vue
new file mode 100644
index 0000000..40dc0b3
--- /dev/null
+++ b/rsf-design/src/views/basic-info/contract/modules/contract-dialog.vue
@@ -0,0 +1,156 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="960px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildContractDialogModel, createContractFormState, getContractStatusOptions } from '../contractPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ contractData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createContractFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫鍚堝悓淇℃伅' : '鏂板鍚堝悓淇℃伅'))
+
+ const rules = computed(() => ({
+ code: [{ required: true, message: '璇疯緭鍏ュ悎鍚岀紪鐮�', trigger: 'blur' }],
+ name: [{ required: true, message: '璇疯緭鍏ュ悎鍚屽悕绉�', trigger: 'blur' }],
+ projectName: [{ required: true, message: '璇疯緭鍏ラ」鐩悕绉�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '鍚堝悓缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ悎鍚岀紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍚堝悓鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ悎鍚屽悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '椤圭洰鍚嶇О',
+ key: 'projectName',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ」鐩悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getContractStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildContractDialogModel(props.contractData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createContractFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.contractData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/device-bind/deviceBindPage.helpers.js b/rsf-design/src/views/basic-info/device-bind/deviceBindPage.helpers.js
new file mode 100644
index 0000000..2f1343e
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-bind/deviceBindPage.helpers.js
@@ -0,0 +1,314 @@
+const FLAG_META = {
+ 1: { text: '鏄�', type: 'success', bool: true },
+ 0: { text: '鍚�', type: 'danger', bool: false }
+}
+
+export const DEVICE_BIND_REPORT_TITLE = '璁惧缁戝畾鎶ヨ〃'
+export const DEVICE_BIND_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeFlagValue(value, fallback = '0') {
+ if (value === 1 || value === '1' || value === true) {
+ return '1'
+ }
+ if (value === 0 || value === '0' || value === false) {
+ return '0'
+ }
+ const text = normalizeText(value)
+ return text || fallback
+}
+
+function normalizeFlagText(value) {
+ if (value === 1 || value === '1' || value === true) {
+ return FLAG_META[1].text
+ }
+ if (value === 0 || value === '0' || value === false) {
+ return FLAG_META[0].text
+ }
+ return '--'
+}
+
+function splitStaList(value) {
+ return normalizeText(value)
+ .split(/[;,锛孿n\r]+/g)
+ .map((item) => item.trim())
+ .filter(Boolean)
+}
+
+export function createDeviceBindSearchState() {
+ return {
+ condition: '',
+ currentRow: null,
+ startRow: null,
+ endRow: null,
+ deviceQty: null,
+ startDeviceNo: null,
+ endDeviceNo: null,
+ typeId: '',
+ staList: '',
+ beSimilar: '',
+ emptySimilar: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createDeviceBindFormState() {
+ return {
+ id: void 0,
+ currentRow: void 0,
+ startRow: void 0,
+ endRow: void 0,
+ deviceQty: void 0,
+ startDeviceNo: void 0,
+ endDeviceNo: void 0,
+ staList: '',
+ typeId: void 0,
+ beSimilar: '0',
+ emptySimilar: '0',
+ memo: ''
+ }
+}
+
+export function getDeviceBindPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getDeviceBindFlagOptions() {
+ return [
+ { label: '鏄�', value: '1' },
+ { label: '鍚�', value: '0' }
+ ]
+}
+
+export function getDeviceBindFlagMeta(value) {
+ if (value === true || Number(value) === 1) {
+ return FLAG_META[1]
+ }
+ if (value === false || Number(value) === 0) {
+ return FLAG_META[0]
+ }
+ return { text: '--', type: 'info', bool: void 0 }
+}
+
+export function resolveDeviceBindTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.areaId ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.name || item.areaName || item.code || item.areaCode || `搴撳尯 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function buildDeviceBindSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ currentRow: normalizeNumber(params.currentRow),
+ startRow: normalizeNumber(params.startRow),
+ endRow: normalizeNumber(params.endRow),
+ deviceQty: normalizeNumber(params.deviceQty),
+ startDeviceNo: normalizeNumber(params.startDeviceNo),
+ endDeviceNo: normalizeNumber(params.endDeviceNo),
+ typeId: normalizeNumber(params.typeId),
+ staList: normalizeText(params.staList),
+ beSimilar: normalizeText(params.beSimilar),
+ emptySimilar: normalizeText(params.emptySimilar),
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildDeviceBindPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildDeviceBindSearchParams(params)
+ }
+}
+
+export function buildDeviceBindSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.currentRow !== void 0 && formData.currentRow !== null && formData.currentRow !== ''
+ ? { currentRow: Number(formData.currentRow) }
+ : {}),
+ ...(formData.startRow !== void 0 && formData.startRow !== null && formData.startRow !== ''
+ ? { startRow: Number(formData.startRow) }
+ : {}),
+ ...(formData.endRow !== void 0 && formData.endRow !== null && formData.endRow !== ''
+ ? { endRow: Number(formData.endRow) }
+ : {}),
+ ...(formData.deviceQty !== void 0 && formData.deviceQty !== null && formData.deviceQty !== ''
+ ? { deviceQty: Number(formData.deviceQty) }
+ : {}),
+ ...(formData.startDeviceNo !== void 0 && formData.startDeviceNo !== null && formData.startDeviceNo !== ''
+ ? { startDeviceNo: Number(formData.startDeviceNo) }
+ : {}),
+ ...(formData.endDeviceNo !== void 0 && formData.endDeviceNo !== null && formData.endDeviceNo !== ''
+ ? { endDeviceNo: Number(formData.endDeviceNo) }
+ : {}),
+ staList: normalizeText(formData.staList) || '',
+ ...(formData.typeId !== void 0 && formData.typeId !== null && formData.typeId !== ''
+ ? { typeId: Number(formData.typeId) }
+ : {}),
+ beSimilar: normalizeFlagValue(formData.beSimilar),
+ emptySimilar: normalizeFlagValue(formData.emptySimilar),
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildDeviceBindDialogModel(record = {}) {
+ return {
+ ...createDeviceBindFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ currentRow:
+ record.currentRow !== void 0 && record.currentRow !== null && record.currentRow !== ''
+ ? Number(record.currentRow)
+ : void 0,
+ startRow:
+ record.startRow !== void 0 && record.startRow !== null && record.startRow !== ''
+ ? Number(record.startRow)
+ : void 0,
+ endRow:
+ record.endRow !== void 0 && record.endRow !== null && record.endRow !== ''
+ ? Number(record.endRow)
+ : void 0,
+ deviceQty:
+ record.deviceQty !== void 0 && record.deviceQty !== null && record.deviceQty !== ''
+ ? Number(record.deviceQty)
+ : void 0,
+ startDeviceNo:
+ record.startDeviceNo !== void 0 && record.startDeviceNo !== null && record.startDeviceNo !== ''
+ ? Number(record.startDeviceNo)
+ : void 0,
+ endDeviceNo:
+ record.endDeviceNo !== void 0 && record.endDeviceNo !== null && record.endDeviceNo !== ''
+ ? Number(record.endDeviceNo)
+ : void 0,
+ staList: normalizeText(record.staList || ''),
+ typeId:
+ record.typeId !== void 0 && record.typeId !== null && record.typeId !== ''
+ ? Number(record.typeId)
+ : void 0,
+ beSimilar: normalizeFlagValue(record.beSimilar, '0'),
+ emptySimilar: normalizeFlagValue(record.emptySimilar, '0'),
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeDeviceBindDetailRecord(record = {}, resolveTypeLabel) {
+ const beSimilarMeta = getDeviceBindFlagMeta(record.beSimilar)
+ const emptySimilarMeta = getDeviceBindFlagMeta(record.emptySimilar)
+
+ return {
+ ...record,
+ currentRow:
+ record.currentRow !== void 0 && record.currentRow !== null && record.currentRow !== ''
+ ? Number(record.currentRow)
+ : void 0,
+ startRow:
+ record.startRow !== void 0 && record.startRow !== null && record.startRow !== ''
+ ? Number(record.startRow)
+ : void 0,
+ endRow:
+ record.endRow !== void 0 && record.endRow !== null && record.endRow !== ''
+ ? Number(record.endRow)
+ : void 0,
+ deviceQty:
+ record.deviceQty !== void 0 && record.deviceQty !== null && record.deviceQty !== ''
+ ? Number(record.deviceQty)
+ : void 0,
+ startDeviceNo:
+ record.startDeviceNo !== void 0 && record.startDeviceNo !== null && record.startDeviceNo !== ''
+ ? Number(record.startDeviceNo)
+ : void 0,
+ endDeviceNo:
+ record.endDeviceNo !== void 0 && record.endDeviceNo !== null && record.endDeviceNo !== ''
+ ? Number(record.endDeviceNo)
+ : void 0,
+ staList: normalizeText(record.staList || ''),
+ staListItems: splitStaList(record.staList),
+ typeIdText:
+ normalizeText(record.typeId$ || record.typeIdText || '') ||
+ (typeof resolveTypeLabel === 'function' ? normalizeText(resolveTypeLabel(record.typeId)) : ''),
+ beSimilarText: beSimilarMeta.text,
+ beSimilarType: beSimilarMeta.type,
+ emptySimilarText: emptySimilarMeta.text,
+ emptySimilarType: emptySimilarMeta.type,
+ memo: normalizeText(record.memo || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeDeviceBindListRow(record = {}, resolveTypeLabel) {
+ return normalizeDeviceBindDetailRecord(record, resolveTypeLabel)
+}
+
+export function buildDeviceBindPrintRows(records = [], resolveTypeLabel) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeDeviceBindListRow(record, resolveTypeLabel))
+}
+
+export function buildDeviceBindReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = DEVICE_BIND_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: DEVICE_BIND_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...DEVICE_BIND_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/device-bind/deviceBindTable.columns.js b/rsf-design/src/views/basic-info/device-bind/deviceBindTable.columns.js
new file mode 100644
index 0000000..9238ee6
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-bind/deviceBindTable.columns.js
@@ -0,0 +1,141 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getDeviceBindFlagMeta } from './deviceBindPage.helpers'
+
+export function createDeviceBindTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ resolveTypeLabel,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'currentRow',
+ label: '褰撳墠鎺掑彿',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.currentRow ?? '--'
+ },
+ {
+ prop: 'startRow',
+ label: '璧峰鎺掑彿',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.startRow ?? '--'
+ },
+ {
+ prop: 'endRow',
+ label: '缁堟鎺掑彿',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.endRow ?? '--'
+ },
+ {
+ prop: 'deviceQty',
+ label: '璁惧鏁伴噺',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.deviceQty ?? '--'
+ },
+ {
+ prop: 'startDeviceNo',
+ label: '璧峰璁惧鍙�',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.startDeviceNo ?? '--'
+ },
+ {
+ prop: 'endDeviceNo',
+ label: '缁堟璁惧鍙�',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.endDeviceNo ?? '--'
+ },
+ {
+ prop: 'staList',
+ label: '绔欑偣鍒楄〃',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.staList || '--'
+ },
+ {
+ prop: 'typeIdText',
+ label: '搴撳尯绫诲瀷',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) =>
+ row.typeIdText || (typeof resolveTypeLabel === 'function' ? resolveTypeLabel(row.typeId) : '') || '--'
+ },
+ {
+ prop: 'beSimilar',
+ label: '鐗╂枡鐩镐技',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getDeviceBindFlagMeta(row.beSimilar)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'emptySimilar',
+ label: '绌烘澘闈犺繎',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getDeviceBindFlagMeta(row.emptySimilar)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/device-bind/index.vue b/rsf-design/src/views/basic-info/device-bind/index.vue
new file mode 100644
index 0000000..091c9b4
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-bind/index.vue
@@ -0,0 +1,434 @@
+<template>
+ <div class="device-bind-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板缁戝畾</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <DeviceBindDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :device-bind-data="currentDeviceBindData"
+ :type-options="typeOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <DeviceBindDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
+ import {
+ fetchDeleteDeviceBind,
+ fetchDeviceBindPage,
+ fetchExportDeviceBindReport,
+ fetchGetDeviceBindDetail,
+ fetchGetDeviceBindMany,
+ fetchSaveDeviceBind,
+ fetchUpdateDeviceBind
+ } from '@/api/device-bind'
+ import DeviceBindDialog from './modules/device-bind-dialog.vue'
+ import DeviceBindDetailDrawer from './modules/device-bind-detail-drawer.vue'
+ import { createDeviceBindTableColumns } from './deviceBindTable.columns'
+ import {
+ buildDeviceBindDialogModel,
+ buildDeviceBindPageQueryParams,
+ buildDeviceBindPrintRows,
+ buildDeviceBindReportMeta,
+ buildDeviceBindSavePayload,
+ buildDeviceBindSearchParams,
+ createDeviceBindSearchState,
+ getDeviceBindPaginationKey,
+ normalizeDeviceBindListRow,
+ resolveDeviceBindTypeOptions,
+ DEVICE_BIND_REPORT_STYLE,
+ DEVICE_BIND_REPORT_TITLE
+ } from './deviceBindPage.helpers'
+
+ defineOptions({ name: 'DeviceBind' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createDeviceBindSearchState())
+ const typeOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = DEVICE_BIND_REPORT_TITLE
+ const reportQueryParams = computed(() => buildDeviceBindSearchParams(searchForm.value))
+ const typeLabelMap = computed(
+ () =>
+ new Map(
+ typeOptions.value
+ .map((item) => [String(item.value), item.label])
+ .filter(([value, label]) => value && label)
+ )
+ )
+
+ function resolveTypeLabel(id) {
+ return typeLabelMap.value.get(String(id)) || ''
+ }
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ帓鍙�/璁惧鍙�/绔欑偣鍒楄〃/澶囨敞'
+ }
+ },
+ {
+ label: '褰撳墠鎺掑彿',
+ key: 'currentRow',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ綋鍓嶆帓鍙�'
+ }
+ },
+ {
+ label: '璧峰鎺掑彿',
+ key: 'startRow',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ捣濮嬫帓鍙�'
+ }
+ },
+ {
+ label: '缁堟鎺掑彿',
+ key: 'endRow',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ粓姝㈡帓鍙�'
+ }
+ },
+ {
+ label: '璁惧鏁伴噺',
+ key: 'deviceQty',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ澶囨暟閲�'
+ }
+ },
+ {
+ label: '璧峰璁惧鍙�',
+ key: 'startDeviceNo',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ捣濮嬭澶囧彿'
+ }
+ },
+ {
+ label: '缁堟璁惧鍙�',
+ key: 'endDeviceNo',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ粓姝㈣澶囧彿'
+ }
+ },
+ {
+ label: '搴撳尯绫诲瀷',
+ key: 'typeId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: typeOptions.value
+ }
+ },
+ {
+ label: '绔欑偣鍒楄〃',
+ key: 'staList',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐瑰垪琛�'
+ }
+ },
+ {
+ label: '鐗╂枡鐩镐技',
+ key: 'beSimilar',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鏄�', value: '1' },
+ { label: '鍚�', value: '0' }
+ ]
+ }
+ },
+ {
+ label: '绌烘澘闈犺繎',
+ key: 'emptySimilar',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鏄�', value: '1' },
+ { label: '鍚�', value: '0' }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨寮�濮嬫椂闂�'
+ }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨缁撴潫鏃堕棿'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetDeviceBindDetail(row.id), {}, {
+ timeoutMessage: '璁惧缁戝畾璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeDeviceBindListRow(detail, resolveTypeLabel)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇璁惧缁戝畾璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetDeviceBindDetail(row.id), {}, {
+ timeoutMessage: '璁惧缁戝畾璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇璁惧缁戝畾璇︽儏澶辫触')
+ }
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchDeviceBindPage,
+ apiParams: buildDeviceBindPageQueryParams(searchForm.value),
+ paginationKey: getDeviceBindPaginationKey(),
+ columnsFactory: () =>
+ createDeviceBindTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ resolveTypeLabel,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeDeviceBindListRow(item, resolveTypeLabel))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentDeviceBindData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildDeviceBindDialogModel(),
+ buildEditModel: (record) => buildDeviceBindDialogModel(record),
+ buildSavePayload: (formData) => buildDeviceBindSavePayload(formData),
+ saveRequest: fetchSaveDeviceBind,
+ updateRequest: fetchUpdateDeviceBind,
+ deleteRequest: fetchDeleteDeviceBind,
+ entityName: '璁惧缁戝畾',
+ resolveRecordLabel: (record) => record?.currentRow ?? record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...DEVICE_BIND_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetDeviceBindMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchDeviceBindPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'device-bind.xlsx',
+ requestExport: (payload) =>
+ fetchExportDeviceBindReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildDeviceBindPrintRows(records, resolveTypeLabel),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildDeviceBindReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || DEVICE_BIND_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildDeviceBindSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createDeviceBindSearchState())
+ resetSearchParams()
+ }
+
+ async function loadTypeOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ typeOptions.value = resolveDeviceBindTypeOptions(defaultResponseAdapter(records).records)
+ }
+
+ onMounted(async () => {
+ await Promise.all([loadTypeOptions(), getData()])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/device-bind/modules/device-bind-detail-drawer.vue b/rsf-design/src/views/basic-info/device-bind/modules/device-bind-detail-drawer.vue
new file mode 100644
index 0000000..8a067e0
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-bind/modules/device-bind-detail-drawer.vue
@@ -0,0 +1,79 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="璁惧缁戝畾璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="褰撳墠鎺掑彿">{{ detail.currentRow ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璧峰鎺掑彿">{{ detail.startRow ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁堟鎺掑彿">{{ detail.endRow ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁惧鏁伴噺">{{ detail.deviceQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璧峰璁惧鍙�">{{ detail.startDeviceNo ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁堟璁惧鍙�">{{ detail.endDeviceNo ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯绫诲瀷">{{ detail.typeIdText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣鍒楄〃" :span="2">
+ <div v-if="staListItems.length" class="flex flex-wrap gap-2">
+ <ElTag v-for="item in staListItems" :key="item" effect="plain">{{ item }}</ElTag>
+ </div>
+ <span v-else>--</span>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鐩镐技">
+ <ElTag :type="detail.beSimilarType || 'info'" effect="light">
+ {{ detail.beSimilarText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="绌烘澘闈犺繎">
+ <ElTag :type="detail.emptySimilarType || 'info'" effect="light">
+ {{ detail.emptySimilarText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ const staListItems = computed(() => {
+ if (Array.isArray(props.detail?.staListItems) && props.detail.staListItems.length) {
+ return props.detail.staListItems
+ }
+ const text = String(props.detail?.staList || '').trim()
+ return text ? text.split(/[;,锛孿n\r]+/g).map((item) => item.trim()).filter(Boolean) : []
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/device-bind/modules/device-bind-dialog.vue b/rsf-design/src/views/basic-info/device-bind/modules/device-bind-dialog.vue
new file mode 100644
index 0000000..20ea4c1
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-bind/modules/device-bind-dialog.vue
@@ -0,0 +1,235 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="920px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildDeviceBindDialogModel,
+ createDeviceBindFormState,
+ getDeviceBindFlagOptions
+ } from '../deviceBindPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ deviceBindData: { type: Object, default: () => ({}) },
+ typeOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createDeviceBindFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫璁惧缁戝畾' : '鏂板璁惧缁戝畾'))
+
+ const rules = computed(() => ({
+ currentRow: [{ required: true, message: '璇疯緭鍏ュ綋鍓嶆帓鍙�', trigger: 'blur' }],
+ startRow: [{ required: true, message: '璇疯緭鍏ヨ捣濮嬫帓鍙�', trigger: 'blur' }],
+ endRow: [{ required: true, message: '璇疯緭鍏ョ粓姝㈡帓鍙�', trigger: 'blur' }],
+ deviceQty: [{ required: true, message: '璇疯緭鍏ヨ澶囨暟閲�', trigger: 'blur' }],
+ startDeviceNo: [{ required: true, message: '璇疯緭鍏ヨ捣濮嬭澶囧彿', trigger: 'blur' }],
+ endDeviceNo: [{ required: true, message: '璇疯緭鍏ョ粓姝㈣澶囧彿', trigger: 'blur' }],
+ staList: [{ required: true, message: '璇疯緭鍏ョ珯鐐瑰垪琛�', trigger: 'blur' }],
+ typeId: [{ required: true, message: '璇烽�夋嫨搴撳尯绫诲瀷', trigger: 'change' }],
+ beSimilar: [{ required: true, message: '璇烽�夋嫨鐗╂枡鐩镐技', trigger: 'change' }],
+ emptySimilar: [{ required: true, message: '璇烽�夋嫨绌烘澘闈犺繎', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '褰撳墠鎺掑彿',
+ key: 'currentRow',
+ type: 'number',
+ props: {
+ controlsPosition: 'right',
+ min: 0,
+ placeholder: '璇疯緭鍏ュ綋鍓嶆帓鍙�'
+ }
+ },
+ {
+ label: '璧峰鎺掑彿',
+ key: 'startRow',
+ type: 'number',
+ props: {
+ controlsPosition: 'right',
+ min: 0,
+ placeholder: '璇疯緭鍏ヨ捣濮嬫帓鍙�'
+ }
+ },
+ {
+ label: '缁堟鎺掑彿',
+ key: 'endRow',
+ type: 'number',
+ props: {
+ controlsPosition: 'right',
+ min: 0,
+ placeholder: '璇疯緭鍏ョ粓姝㈡帓鍙�'
+ }
+ },
+ {
+ label: '璁惧鏁伴噺',
+ key: 'deviceQty',
+ type: 'number',
+ props: {
+ controlsPosition: 'right',
+ min: 0,
+ placeholder: '璇疯緭鍏ヨ澶囨暟閲�'
+ }
+ },
+ {
+ label: '璧峰璁惧鍙�',
+ key: 'startDeviceNo',
+ type: 'number',
+ props: {
+ controlsPosition: 'right',
+ min: 0,
+ placeholder: '璇疯緭鍏ヨ捣濮嬭澶囧彿'
+ }
+ },
+ {
+ label: '缁堟璁惧鍙�',
+ key: 'endDeviceNo',
+ type: 'number',
+ props: {
+ controlsPosition: 'right',
+ min: 0,
+ placeholder: '璇疯緭鍏ョ粓姝㈣澶囧彿'
+ }
+ },
+ {
+ label: '绔欑偣鍒楄〃',
+ key: 'staList',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ョ珯鐐瑰垪琛紝澶氫釜绔欑偣鍙敤鍒嗗彿鍒嗛殧',
+ clearable: true
+ }
+ },
+ {
+ label: '搴撳尯绫诲瀷',
+ key: 'typeId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撳尯绫诲瀷',
+ clearable: true,
+ filterable: true,
+ options: props.typeOptions || []
+ }
+ },
+ {
+ label: '鐗╂枡鐩镐技',
+ key: 'beSimilar',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐗╂枡鐩镐技',
+ clearable: true,
+ options: getDeviceBindFlagOptions()
+ }
+ },
+ {
+ label: '绌烘澘闈犺繎',
+ key: 'emptySimilar',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨绌烘澘闈犺繎',
+ clearable: true,
+ options: getDeviceBindFlagOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildDeviceBindDialogModel(props.deviceBindData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createDeviceBindFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.deviceBindData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/device-site/deviceSitePage.helpers.js b/rsf-design/src/views/basic-info/device-site/deviceSitePage.helpers.js
new file mode 100644
index 0000000..820bf18
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-site/deviceSitePage.helpers.js
@@ -0,0 +1,442 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const DEVICE_SITE_REPORT_TITLE = '璺緞绠$悊鎶ヨ〃'
+export const DEVICE_SITE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeArrayValue(value) {
+ if (Array.isArray(value)) {
+ return value.map((item) => normalizeText(item)).filter(Boolean)
+ }
+ const text = normalizeText(value)
+ if (!text) {
+ return []
+ }
+ return text
+ .split(',')
+ .map((item) => item.trim())
+ .filter(Boolean)
+}
+
+function normalizeSelectOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: normalizeText(value),
+ label: normalizeText(item.label || item.name || item.dictLabel || item.value || `閫夐」 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function createDeviceSiteSearchState() {
+ return {
+ condition: '',
+ type: '',
+ site: '',
+ name: '',
+ target: '',
+ label: '',
+ device: '',
+ deviceCode: '',
+ deviceSite: '',
+ channel: '',
+ areaIdStart: '',
+ areaIdEnd: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createDeviceSiteFormState() {
+ return {
+ id: void 0,
+ type: [],
+ site: '',
+ name: '',
+ target: '',
+ label: '',
+ device: '',
+ deviceCode: '',
+ deviceSite: '',
+ channel: void 0,
+ areaIdStart: void 0,
+ areaIdEnd: void 0,
+ status: 1,
+ memo: ''
+ }
+}
+
+export function createDeviceSiteInitState() {
+ return {
+ flagInit: 0,
+ deviceType: '',
+ typeIds: [],
+ site: '',
+ deviceCode: '',
+ deviceSites: '',
+ target: '',
+ rows: [],
+ channel: '',
+ areaIdStart: void 0,
+ areaIdEnd: void 0,
+ name: '',
+ wcsCode: '',
+ label: ''
+ }
+}
+
+export function getDeviceSitePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getDeviceSiteStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getDeviceSiteStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildDeviceSiteSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ type: normalizeArrayValue(params.type).join(','),
+ site: normalizeText(params.site),
+ name: normalizeText(params.name),
+ target: normalizeText(params.target),
+ label: normalizeText(params.label),
+ device: normalizeText(params.device),
+ deviceCode: normalizeText(params.deviceCode),
+ deviceSite: normalizeText(params.deviceSite),
+ channel:
+ params.channel !== undefined && params.channel !== null && params.channel !== ''
+ ? Number(params.channel)
+ : void 0,
+ areaIdStart:
+ params.areaIdStart !== undefined && params.areaIdStart !== null && params.areaIdStart !== ''
+ ? Number(params.areaIdStart)
+ : void 0,
+ areaIdEnd:
+ params.areaIdEnd !== undefined && params.areaIdEnd !== null && params.areaIdEnd !== ''
+ ? Number(params.areaIdEnd)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildDeviceSitePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildDeviceSiteSearchParams(params)
+ }
+}
+
+export function buildDeviceSiteSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ type: normalizeArrayValue(formData.type).join(','),
+ site: normalizeText(formData.site) || '',
+ name: normalizeText(formData.name) || '',
+ target: normalizeText(formData.target) || '',
+ label: normalizeText(formData.label) || '',
+ device: normalizeText(formData.device) || '',
+ deviceCode: normalizeText(formData.deviceCode) || '',
+ deviceSite: normalizeText(formData.deviceSite) || '',
+ ...(formData.channel !== void 0 && formData.channel !== null && formData.channel !== ''
+ ? { channel: Number(formData.channel) }
+ : {}),
+ ...(formData.areaIdStart !== void 0 && formData.areaIdStart !== null && formData.areaIdStart !== ''
+ ? { areaIdStart: Number(formData.areaIdStart) }
+ : {}),
+ ...(formData.areaIdEnd !== void 0 && formData.areaIdEnd !== null && formData.areaIdEnd !== ''
+ ? { areaIdEnd: Number(formData.areaIdEnd) }
+ : {}),
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildDeviceSiteInitPayload(formData = {}) {
+ return {
+ flagInit:
+ formData.flagInit !== void 0 && formData.flagInit !== null && formData.flagInit !== ''
+ ? Number(formData.flagInit)
+ : 0,
+ deviceType: normalizeText(formData.deviceType) || '',
+ typeIds: Array.isArray(formData.typeIds) ? formData.typeIds.map((item) => normalizeText(item)).filter(Boolean) : [],
+ site: normalizeText(formData.site) || '',
+ deviceCode: normalizeText(formData.deviceCode) || '',
+ deviceSites: normalizeText(formData.deviceSites) || '',
+ target: normalizeText(formData.target) || '',
+ rows: Array.isArray(formData.rows)
+ ? formData.rows
+ .map((row) => ({
+ deviceSite: normalizeText(row?.deviceSite) || '',
+ site: normalizeText(row?.site) || '',
+ target: normalizeText(row?.target) || ''
+ }))
+ .filter((row) => row.deviceSite && row.site && row.target)
+ : [],
+ channel: normalizeText(formData.channel) || '',
+ ...(formData.areaIdStart !== void 0 && formData.areaIdStart !== null && formData.areaIdStart !== ''
+ ? { areaIdStart: Number(formData.areaIdStart) }
+ : {}),
+ ...(formData.areaIdEnd !== void 0 && formData.areaIdEnd !== null && formData.areaIdEnd !== ''
+ ? { areaIdEnd: Number(formData.areaIdEnd) }
+ : {}),
+ name: normalizeText(formData.name) || '',
+ wcsCode: normalizeText(formData.wcsCode) || '',
+ label: normalizeText(formData.label) || ''
+ }
+}
+
+export function buildDeviceSiteDialogModel(record = {}) {
+ return {
+ ...createDeviceSiteFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ type: normalizeArrayValue(record.type ?? record.type$ ?? []),
+ site: normalizeText(record.site || ''),
+ name: normalizeText(record.name || ''),
+ target: normalizeText(record.target || ''),
+ label: normalizeText(record.label || ''),
+ device: normalizeText(record.device || ''),
+ deviceCode: normalizeText(record.deviceCode || ''),
+ deviceSite: normalizeText(record.deviceSite || ''),
+ channel:
+ record.channel !== void 0 && record.channel !== null && record.channel !== ''
+ ? Number(record.channel)
+ : void 0,
+ areaIdStart:
+ record.areaIdStart !== void 0 && record.areaIdStart !== null && record.areaIdStart !== ''
+ ? Number(record.areaIdStart)
+ : void 0,
+ areaIdEnd:
+ record.areaIdEnd !== void 0 && record.areaIdEnd !== null && record.areaIdEnd !== ''
+ ? Number(record.areaIdEnd)
+ : void 0,
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function buildDeviceSiteInitModel(record = {}) {
+ return {
+ ...createDeviceSiteInitState(),
+ flagInit:
+ record.flagInit !== void 0 && record.flagInit !== null && record.flagInit !== ''
+ ? Number(record.flagInit)
+ : 0,
+ deviceType: normalizeText(record.deviceType || ''),
+ typeIds: normalizeArrayValue(record.typeIds ?? record.type ?? []),
+ site: normalizeText(record.site || ''),
+ deviceCode: normalizeText(record.deviceCode || ''),
+ deviceSites: normalizeText(record.deviceSites || ''),
+ target: normalizeText(record.target || ''),
+ rows: Array.isArray(record.rows)
+ ? record.rows.map((row) => ({
+ deviceSite: normalizeText(row?.deviceSite || row?.deviceSiteName || ''),
+ site: normalizeText(row?.site || row?.siteName || ''),
+ target: normalizeText(row?.target || '')
+ }))
+ : [],
+ channel: normalizeText(record.channel || ''),
+ areaIdStart:
+ record.areaIdStart !== void 0 && record.areaIdStart !== null && record.areaIdStart !== ''
+ ? Number(record.areaIdStart)
+ : void 0,
+ areaIdEnd:
+ record.areaIdEnd !== void 0 && record.areaIdEnd !== null && record.areaIdEnd !== ''
+ ? Number(record.areaIdEnd)
+ : void 0,
+ name: normalizeText(record.name || ''),
+ wcsCode: normalizeText(record.wcsCode || ''),
+ label: normalizeText(record.label || '')
+ }
+}
+
+export function normalizeDeviceSiteDetailRecord(record = {}, resolveTypeLabel, resolveDeviceLabel, resolveAreaLabel) {
+ const statusMeta = getDeviceSiteStatusMeta(record.statusBool ?? record.status)
+ const typeText = normalizeText(record.type$ || record.typeText || '')
+ const deviceText = normalizeText(record.device$ || record.deviceText || '')
+ const typeItems = normalizeArrayValue(record.type ?? record.type$ ?? '')
+
+ return {
+ ...record,
+ type: typeItems,
+ typeItems,
+ typeText:
+ typeText ||
+ typeItems
+ .map((item) => {
+ if (typeof resolveTypeLabel === 'function') {
+ const resolved = normalizeText(resolveTypeLabel(item))
+ if (resolved) {
+ return resolved
+ }
+ }
+ return item
+ })
+ .filter(Boolean)
+ .join(','),
+ site: normalizeText(record.site || ''),
+ name: normalizeText(record.name || ''),
+ target: normalizeText(record.target || ''),
+ label: normalizeText(record.label || ''),
+ device: normalizeText(record.device || ''),
+ deviceText:
+ deviceText ||
+ (typeof resolveDeviceLabel === 'function' ? normalizeText(resolveDeviceLabel(record.device)) : ''),
+ deviceCode: normalizeText(record.deviceCode || ''),
+ deviceSite: normalizeText(record.deviceSite || ''),
+ channel: record.channel !== void 0 && record.channel !== null && record.channel !== ''
+ ? Number(record.channel)
+ : void 0,
+ areaIdStartText:
+ normalizeText(record.areaIdStart$ || '') ||
+ (typeof resolveAreaLabel === 'function' ? normalizeText(resolveAreaLabel(record.areaIdStart)) : '') ||
+ normalizeText(record.areaIdStart),
+ areaIdEndText:
+ normalizeText(record.areaIdEnd$ || '') ||
+ (typeof resolveAreaLabel === 'function' ? normalizeText(resolveAreaLabel(record.areaIdEnd)) : '') ||
+ normalizeText(record.areaIdEnd),
+ areaIdStart: record.areaIdStart !== void 0 && record.areaIdStart !== null && record.areaIdStart !== ''
+ ? Number(record.areaIdStart)
+ : void 0,
+ areaIdEnd: record.areaIdEnd !== void 0 && record.areaIdEnd !== null && record.areaIdEnd !== ''
+ ? Number(record.areaIdEnd)
+ : void 0,
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeDeviceSiteListRow(record = {}, resolveTypeLabel, resolveDeviceLabel, resolveAreaLabel) {
+ return normalizeDeviceSiteDetailRecord(record, resolveTypeLabel, resolveDeviceLabel, resolveAreaLabel)
+}
+
+export function buildDeviceSitePrintRows(records = [], resolveTypeLabel, resolveDeviceLabel, resolveAreaLabel) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeDeviceSiteListRow(record, resolveTypeLabel, resolveDeviceLabel, resolveAreaLabel))
+}
+
+export function buildDeviceSiteReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = DEVICE_SITE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: DEVICE_SITE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...DEVICE_SITE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function resolveDeviceSiteTypeOptions(records = []) {
+ return normalizeSelectOptions(records)
+}
+
+export function resolveDeviceSiteDeviceOptions(records = []) {
+ return normalizeSelectOptions(records)
+}
+
+export function resolveDeviceSiteAreaOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.areaId ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ const numberValue = Number(value)
+ if (Number.isNaN(numberValue)) {
+ return null
+ }
+ return {
+ value: numberValue,
+ label: normalizeText(item.label || item.name || item.areaName || item.dictLabel || `搴撳尯 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
diff --git a/rsf-design/src/views/basic-info/device-site/deviceSiteTable.columns.js b/rsf-design/src/views/basic-info/device-site/deviceSiteTable.columns.js
new file mode 100644
index 0000000..939a805
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-site/deviceSiteTable.columns.js
@@ -0,0 +1,150 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getDeviceSiteStatusMeta } from './deviceSitePage.helpers'
+
+export function createDeviceSiteTableColumns({
+ handleView,
+ handleInit,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (handleInit) {
+ operations.push({ key: 'init', label: '鍒濆鍖�', icon: 'ri:route-line' })
+ }
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'typeText',
+ label: '绔欑偣绫诲瀷',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeText || row.type$ || '--'
+ },
+ {
+ prop: 'site',
+ label: '浣滀笟绔欑偣',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.site || '--'
+ },
+ {
+ prop: 'name',
+ label: '鍚嶇О',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.name || '--'
+ },
+ {
+ prop: 'target',
+ label: '鐩爣绔欑偣',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.target || '--'
+ },
+ {
+ prop: 'label',
+ label: '绔欑偣鏍囩',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.label || '--'
+ },
+ {
+ prop: 'deviceText',
+ label: '璁惧绫诲瀷',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.deviceText || row.device$ || row.device || '--'
+ },
+ {
+ prop: 'deviceCode',
+ label: '璁惧缂栧彿',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.deviceCode || '--'
+ },
+ {
+ prop: 'deviceSite',
+ label: '璁惧绔欑偣',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.deviceSite || '--'
+ },
+ {
+ prop: 'channel',
+ label: '宸烽亾',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.channel ?? '--'
+ },
+ {
+ prop: 'areaIdStart',
+ label: '婧愬簱鍖�',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.areaIdStart ?? '--'
+ },
+ {
+ prop: 'areaIdEnd',
+ label: '鐩爣搴撳尯',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.areaIdEnd ?? '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getDeviceSiteStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 180,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'init') handleInit?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/device-site/index.vue b/rsf-design/src/views/basic-info/device-site/index.vue
new file mode 100644
index 0000000..d6736ae
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-site/index.vue
@@ -0,0 +1,439 @@
+<template>
+ <div class="device-site-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板璺緞</ElButton>
+ <ElButton v-auth="'add'" @click="openInitDialog()" v-ripple>璺緞鍒濆鍖�</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <DeviceSiteDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :device-site-data="currentDeviceSiteData"
+ :type-options="typeOptions"
+ :device-options="deviceOptions"
+ :area-options="areaOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <DeviceSiteDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+
+ <DeviceSiteInitDialog
+ v-model:visible="initDialogVisible"
+ :initial-data="currentInitData"
+ :type-options="typeOptions"
+ :device-options="deviceOptions"
+ :area-options="areaOptions"
+ :station-options="stationOptions"
+ @submit="handleInitSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchBasStationPage } from '@/api/bas-station'
+ import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import {
+ fetchDeleteDeviceSite,
+ fetchDeviceSitePage,
+ fetchExportDeviceSiteReport,
+ fetchGetDeviceSiteDetail,
+ fetchGetDeviceSiteMany,
+ fetchInitDeviceSite,
+ fetchSaveDeviceSite,
+ fetchUpdateDeviceSite
+ } from '@/api/device-site'
+ import DeviceSiteDialog from './modules/device-site-dialog.vue'
+ import DeviceSiteDetailDrawer from './modules/device-site-detail-drawer.vue'
+ import DeviceSiteInitDialog from './modules/device-site-init-dialog.vue'
+ import { createDeviceSiteTableColumns } from './deviceSiteTable.columns'
+ import {
+ DEVICE_SITE_REPORT_STYLE,
+ DEVICE_SITE_REPORT_TITLE,
+ buildDeviceSiteDialogModel,
+ buildDeviceSiteInitModel,
+ buildDeviceSiteInitPayload,
+ buildDeviceSitePageQueryParams,
+ buildDeviceSitePrintRows,
+ buildDeviceSiteReportMeta,
+ buildDeviceSiteSavePayload,
+ buildDeviceSiteSearchParams,
+ createDeviceSiteSearchState,
+ getDeviceSitePaginationKey,
+ normalizeDeviceSiteListRow,
+ resolveDeviceSiteAreaOptions,
+ resolveDeviceSiteDeviceOptions,
+ resolveDeviceSiteTypeOptions
+ } from './deviceSitePage.helpers'
+
+ defineOptions({ name: 'DeviceSite' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createDeviceSiteSearchState())
+ const typeOptions = ref([])
+ const deviceOptions = ref([])
+ const areaOptions = ref([])
+ const stationOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const initDialogVisible = ref(false)
+ const currentInitData = ref(buildDeviceSiteInitModel())
+ let handleDeleteAction = null
+
+ const reportTitle = DEVICE_SITE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildDeviceSiteSearchParams(searchForm.value))
+ const typeLabelMap = computed(
+ () =>
+ new Map(typeOptions.value.map((item) => [String(item.value), item.label]).filter(([value, label]) => value && label))
+ )
+ const deviceLabelMap = computed(
+ () =>
+ new Map(deviceOptions.value.map((item) => [String(item.value), item.label]).filter(([value, label]) => value && label))
+ )
+ const areaLabelMap = computed(
+ () =>
+ new Map(areaOptions.value.map((item) => [String(item.value), item.label]).filter(([value, label]) => value && label))
+ )
+
+ function resolveTypeLabel(value) {
+ return typeLabelMap.value.get(String(value)) || ''
+ }
+
+ function resolveDeviceLabel(value) {
+ return deviceLabelMap.value.get(String(value)) || ''
+ }
+
+ function resolveAreaLabel(value) {
+ return areaLabelMap.value.get(String(value)) || ''
+ }
+
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ珯鐐�/鍚嶇О/鐩爣/鏍囩' } },
+ {
+ label: '绔欑偣绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: { clearable: true, filterable: true, multiple: true, collapseTags: true, options: typeOptions.value }
+ },
+ { label: '浣滀笟绔欑偣', key: 'site', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヤ綔涓氱珯鐐�' } },
+ { label: '鍚嶇О', key: 'name', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ悕绉�' } },
+ { label: '鐩爣绔欑偣', key: 'target', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洰鏍囩珯鐐�' } },
+ { label: '绔欑偣鏍囩', key: 'label', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ珯鐐规爣绛�' } },
+ { label: '璁惧绫诲瀷', key: 'device', type: 'select', props: { clearable: true, filterable: true, options: deviceOptions.value } },
+ { label: '璁惧缂栧彿', key: 'deviceCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ澶囩紪鍙�' } },
+ { label: '璁惧绔欑偣', key: 'deviceSite', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ澶囩珯鐐�' } },
+ { label: '宸烽亾', key: 'channel', type: 'number', props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ュ贩閬�' } },
+ { label: '婧愬簱鍖�', key: 'areaIdStart', type: 'number', props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ユ簮搴撳尯' } },
+ { label: '鐩爣搴撳尯', key: 'areaIdEnd', type: 'number', props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ョ洰鏍囧簱鍖�' } },
+ { label: '鐘舵��', key: 'status', type: 'select', props: { clearable: true, options: [{ label: '姝e父', value: 1 }, { label: '鍐荤粨', value: 0 }] } },
+ { label: '澶囨敞', key: 'memo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' } }
+ ])
+
+ function createInitRecordFromRow(row) {
+ return buildDeviceSiteInitModel({
+ deviceType: row.device || '',
+ typeIds: row.type || row.typeText || '',
+ channel: row.channel,
+ areaIdStart: row.areaIdStart,
+ areaIdEnd: row.areaIdEnd,
+ name: row.name,
+ label: row.label,
+ rows: [{ deviceSiteName: row.deviceSite || '', siteName: row.site || '', target: row.target || '' }]
+ })
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetDeviceSiteDetail(row.id), {}, {
+ timeoutMessage: '璺緞璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeDeviceSiteListRow(detail, resolveTypeLabel, resolveDeviceLabel, resolveAreaLabel)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇璺緞璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetDeviceSiteDetail(row.id), {}, {
+ timeoutMessage: '璺緞璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇璺緞璇︽儏澶辫触')
+ }
+ }
+
+ function openInitDialog(record = null) {
+ currentInitData.value = record ? createInitRecordFromRow(record) : buildDeviceSiteInitModel()
+ initDialogVisible.value = true
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchDeviceSitePage,
+ apiParams: buildDeviceSitePageQueryParams(searchForm.value),
+ paginationKey: getDeviceSitePaginationKey(),
+ columnsFactory: () =>
+ createDeviceSiteTableColumns({
+ handleView: openDetail,
+ handleInit: hasAuth('add') ? openInitDialog : null,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeDeviceSiteListRow(item, resolveTypeLabel, resolveDeviceLabel, resolveAreaLabel))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentDeviceSiteData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildDeviceSiteDialogModel(),
+ buildEditModel: (record) => buildDeviceSiteDialogModel(record),
+ buildSavePayload: (formData) => buildDeviceSiteSavePayload(formData),
+ saveRequest: fetchSaveDeviceSite,
+ updateRequest: fetchUpdateDeviceSite,
+ deleteRequest: fetchDeleteDeviceSite,
+ entityName: '璺緞',
+ resolveRecordLabel: (record) => record?.name || record?.site || record?.deviceSite || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...DEVICE_SITE_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetDeviceSiteMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchDeviceSitePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'device-site.xlsx',
+ requestExport: (payload) =>
+ fetchExportDeviceSiteReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildDeviceSitePrintRows(records, resolveTypeLabel, resolveDeviceLabel, resolveAreaLabel),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildDeviceSiteReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || DEVICE_SITE_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildDeviceSiteSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createDeviceSiteSearchState())
+ resetSearchParams()
+ }
+
+ async function handleInitSubmit(formData) {
+ try {
+ await fetchInitDeviceSite(buildDeviceSiteInitPayload(formData))
+ ElMessage.success('鍒濆鍖栨垚鍔�')
+ initDialogVisible.value = false
+ currentInitData.value = buildDeviceSiteInitModel()
+ await refreshData()
+ } catch (error) {
+ ElMessage.error(error?.message || '鍒濆鍖栧け璐�')
+ }
+ }
+
+ function buildSelectOptions(records = [], labelKeys = ['label', 'name', 'stationName']) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ const label = labelKeys.map((key) => item[key]).find((value2) => value2 !== void 0 && value2 !== null && String(value2).trim() !== '')
+ return { value: String(value), label: String(label || value).trim() }
+ })
+ .filter(Boolean)
+ }
+
+ async function loadTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({ current: 1, pageSize: 200, dictTypeCode: 'sys_task_type', status: 1 }),
+ { records: [] },
+ { timeoutMessage: '绔欑偣绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ typeOptions.value = resolveDeviceSiteTypeOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadDeviceOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({ current: 1, pageSize: 200, dictTypeCode: 'sys_device_type', status: 1 }),
+ { records: [] },
+ { timeoutMessage: '璁惧绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ deviceOptions.value = resolveDeviceSiteDeviceOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadAreaOptions() {
+ const response = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveDeviceSiteAreaOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadStationOptions() {
+ const response = await guardRequestWithMessage(
+ fetchBasStationPage({
+ current: 1,
+ pageSize: 500
+ }, {
+ showErrorMessage: false
+ }),
+ { records: [] },
+ {
+ timeoutMessage: '绔欑偣鍒楄〃鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ stationOptions.value = buildSelectOptions(defaultResponseAdapter(response).records, ['stationName', 'name'])
+ }
+
+ onMounted(async () => {
+ await Promise.all([loadTypeOptions(), loadDeviceOptions(), loadAreaOptions(), loadStationOptions()])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/device-site/modules/device-site-detail-drawer.vue b/rsf-design/src/views/basic-info/device-site/modules/device-site-detail-drawer.vue
new file mode 100644
index 0000000..25fc469
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-site/modules/device-site-detail-drawer.vue
@@ -0,0 +1,62 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="璺緞璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="绔欑偣绫诲瀷">{{ detail.typeText || detail.type?.join(',') || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣滀笟绔欑偣">{{ detail.site || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚嶇О">{{ detail.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣绔欑偣">{{ detail.target || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣鏍囩">{{ detail.label || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁惧绫诲瀷">{{ detail.deviceText || detail.device || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁惧缂栧彿">{{ detail.deviceCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁惧绔欑偣">{{ detail.deviceSite || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸烽亾">{{ detail.channel ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愬簱鍖�">{{ detail.areaIdStartText || (detail.areaIdStart ?? '--') }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣搴撳尯">{{ detail.areaIdEndText || (detail.areaIdEnd ?? '--') }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/device-site/modules/device-site-dialog.vue b/rsf-design/src/views/basic-info/device-site/modules/device-site-dialog.vue
new file mode 100644
index 0000000..522fb58
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-site/modules/device-site-dialog.vue
@@ -0,0 +1,137 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="980px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildDeviceSiteDialogModel,
+ createDeviceSiteFormState,
+ getDeviceSiteStatusOptions
+ } from '../deviceSitePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ deviceSiteData: { type: Object, default: () => ({}) },
+ typeOptions: { type: Array, default: () => [] },
+ deviceOptions: { type: Array, default: () => [] },
+ areaOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createDeviceSiteFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫璺緞' : '鏂板璺緞'))
+
+ const rules = computed(() => ({
+ type: [{ type: 'array', required: true, message: '璇烽�夋嫨绔欑偣绫诲瀷', trigger: 'change' }],
+ site: [{ required: true, message: '璇疯緭鍏ヤ綔涓氱珯鐐�', trigger: 'blur' }],
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }],
+ target: [{ required: true, message: '璇疯緭鍏ョ洰鏍囩珯鐐�', trigger: 'blur' }],
+ label: [{ required: true, message: '璇疯緭鍏ョ珯鐐规爣绛�', trigger: 'blur' }],
+ device: [{ required: true, message: '璇烽�夋嫨璁惧绫诲瀷', trigger: 'change' }],
+ deviceCode: [{ required: true, message: '璇疯緭鍏ヨ澶囩紪鍙�', trigger: 'blur' }],
+ deviceSite: [{ required: true, message: '璇疯緭鍏ヨ澶囩珯鐐�', trigger: 'blur' }],
+ channel: [{ required: true, message: '璇疯緭鍏ュ贩閬�', trigger: 'blur' }],
+ areaIdStart: [{ required: true, message: '璇烽�夋嫨婧愬簱鍖�', trigger: 'change' }],
+ areaIdEnd: [{ required: true, message: '璇烽�夋嫨鐩爣搴撳尯', trigger: 'change' }],
+ status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ { label: '绔欑偣绫诲瀷', key: 'type', type: 'select', span: 24, props: { placeholder: '璇烽�夋嫨绔欑偣绫诲瀷', clearable: true, multiple: true, collapseTags: true, filterable: true, options: props.typeOptions || [] } },
+ { label: '浣滀笟绔欑偣', key: 'site', type: 'input', props: { placeholder: '璇疯緭鍏ヤ綔涓氱珯鐐�', clearable: true } },
+ { label: '鍚嶇О', key: 'name', type: 'input', props: { placeholder: '璇疯緭鍏ュ悕绉�', clearable: true } },
+ { label: '鐩爣绔欑偣', key: 'target', type: 'input', props: { placeholder: '璇疯緭鍏ョ洰鏍囩珯鐐�', clearable: true } },
+ { label: '绔欑偣鏍囩', key: 'label', type: 'input', props: { placeholder: '璇疯緭鍏ョ珯鐐规爣绛�', clearable: true } },
+ { label: '璁惧绫诲瀷', key: 'device', type: 'select', props: { placeholder: '璇烽�夋嫨璁惧绫诲瀷', clearable: true, filterable: true, options: props.deviceOptions || [] } },
+ { label: '璁惧缂栧彿', key: 'deviceCode', type: 'input', props: { placeholder: '璇疯緭鍏ヨ澶囩紪鍙�', clearable: true } },
+ { label: '璁惧绔欑偣', key: 'deviceSite', type: 'input', props: { placeholder: '璇疯緭鍏ヨ澶囩珯鐐�', clearable: true } },
+ { label: '宸烽亾', key: 'channel', type: 'input', props: { placeholder: '璇疯緭鍏ュ贩閬�', clearable: true } },
+ { label: '婧愬簱鍖�', key: 'areaIdStart', type: 'select', props: { placeholder: '璇烽�夋嫨婧愬簱鍖�', clearable: true, filterable: true, options: props.areaOptions || [] } },
+ { label: '鐩爣搴撳尯', key: 'areaIdEnd', type: 'select', props: { placeholder: '璇烽�夋嫨鐩爣搴撳尯', clearable: true, filterable: true, options: props.areaOptions || [] } },
+ { label: '鐘舵��', key: 'status', type: 'select', props: { placeholder: '璇烽�夋嫨鐘舵��', clearable: true, options: getDeviceSiteStatusOptions() } },
+ { label: '澶囨敞', key: 'memo', type: 'input', span: 24, props: { type: 'textarea', rows: 3, placeholder: '璇疯緭鍏ュ娉�', clearable: true } }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildDeviceSiteDialogModel(props.deviceSiteData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createDeviceSiteFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.deviceSiteData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/device-site/modules/device-site-init-dialog.vue b/rsf-design/src/views/basic-info/device-site/modules/device-site-init-dialog.vue
new file mode 100644
index 0000000..0e9de17
--- /dev/null
+++ b/rsf-design/src/views/basic-info/device-site/modules/device-site-init-dialog.vue
@@ -0,0 +1,212 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="1120px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <div class="deviceSite-init-row mt-4">
+ <div class="mb-3 flex items-center justify-between">
+ <div class="text-sm font-medium text-[var(--art-gray-900)]">璺緞琛屽垪琛�</div>
+ <ElButton size="small" @click="addRow">鏂板涓�琛�</ElButton>
+ </div>
+ <ElTable :data="form.rows" border>
+ <ElTableColumn label="璁惧绔欑偣" min-width="220">
+ <template #default="{ row }">
+ <ElSelect v-model="row.deviceSite" class="w-full" filterable clearable placeholder="璇烽�夋嫨璁惧绔欑偣">
+ <ElOption v-for="item in stationOptions" :key="item.value" :label="item.label" :value="item.value" />
+ </ElSelect>
+ </template>
+ </ElTableColumn>
+ <ElTableColumn label="浣滀笟绔欑偣" min-width="220">
+ <template #default="{ row }">
+ <ElSelect v-model="row.site" class="w-full" filterable clearable placeholder="璇烽�夋嫨浣滀笟绔欑偣">
+ <ElOption v-for="item in stationOptions" :key="item.value" :label="item.label" :value="item.value" />
+ </ElSelect>
+ </template>
+ </ElTableColumn>
+ <ElTableColumn label="鐩爣绔欑偣" min-width="220">
+ <template #default="{ row }">
+ <ElInput v-model="row.target" clearable placeholder="璇疯緭鍏ョ洰鏍囩珯鐐�" />
+ </template>
+ </ElTableColumn>
+ <ElTableColumn label="鎿嶄綔" width="100" align="center">
+ <template #default="{ $index }">
+ <ElButton link type="danger" :disabled="form.rows.length <= 1" @click="removeRow($index)">鍒犻櫎</ElButton>
+ </template>
+ </ElTableColumn>
+ </ElTable>
+ <div class="mt-2 text-xs text-[var(--art-gray-500)]">
+ 姣忚琛ㄧず涓�缁勮澶囩珯鐐广�佷綔涓氱珯鐐广�佺洰鏍囩珯鐐广��
+ </div>
+ </div>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildDeviceSiteInitModel, createDeviceSiteInitState } from '../deviceSitePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ initialData: { type: Object, default: () => ({}) },
+ typeOptions: { type: Array, default: () => [] },
+ deviceOptions: { type: Array, default: () => [] },
+ areaOptions: { type: Array, default: () => [] },
+ stationOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createDeviceSiteInitState())
+
+ const dialogTitle = computed(() => '璺緞鍒濆鍖�')
+
+ const rules = computed(() => ({
+ typeIds: [{ type: 'array', required: true, message: '璇烽�夋嫨浣滀笟绫诲瀷', trigger: 'change' }],
+ deviceType: [{ required: true, message: '璇烽�夋嫨璁惧绫诲瀷', trigger: 'change' }],
+ channel: [{ required: true, message: '璇疯緭鍏ュ贩閬�', trigger: 'blur' }],
+ areaIdStart: [{ required: true, message: '璇烽�夋嫨婧愬簱鍖�', trigger: 'change' }],
+ areaIdEnd: [{ required: true, message: '璇烽�夋嫨鐩爣搴撳尯', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ { label: '鏄惁鍒濆鍖�', key: 'flagInit', type: 'select', props: { clearable: false, options: [{ label: '鍚�', value: 0 }, { label: '鏄�', value: 1 }] } },
+ { label: '璁惧绫诲瀷', key: 'deviceType', type: 'select', props: { placeholder: '璇烽�夋嫨璁惧绫诲瀷', clearable: true, filterable: true, options: props.deviceOptions || [] } },
+ { label: '浣滀笟绫诲瀷', key: 'typeIds', type: 'select', span: 24, props: { placeholder: '璇烽�夋嫨浣滀笟绫诲瀷', clearable: true, multiple: true, collapseTags: true, filterable: true, options: props.typeOptions || [] } },
+ { label: '宸烽亾', key: 'channel', type: 'input', props: { placeholder: '璇疯緭鍏ュ贩閬擄紝澶氫釜璇风敤鑻辨枃閫楀彿鍒嗛殧', clearable: true } },
+ { label: '婧愬簱鍖�', key: 'areaIdStart', type: 'select', props: { placeholder: '璇烽�夋嫨婧愬簱鍖�', clearable: true, filterable: true, options: props.areaOptions || [] } },
+ { label: '鐩爣搴撳尯', key: 'areaIdEnd', type: 'select', props: { placeholder: '璇烽�夋嫨鐩爣搴撳尯', clearable: true, filterable: true, options: props.areaOptions || [] } },
+ { label: '鍚嶇О', key: 'name', type: 'input', props: { placeholder: '璇疯緭鍏ュ悕绉�', clearable: true } },
+ { label: '绔欑偣鏍囩', key: 'label', type: 'input', props: { placeholder: '璇疯緭鍏ョ珯鐐规爣绛�', clearable: true } }
+ ])
+
+ function resolveStationValue(rawValue) {
+ const text = String(rawValue ?? '').trim()
+ if (!text) {
+ return ''
+ }
+ const exact = props.stationOptions.find((item) => String(item.value) === text)
+ if (exact) {
+ return exact.value
+ }
+ const matched = props.stationOptions.find((item) => item.label === text)
+ return matched ? matched.value : text
+ }
+
+ function normalizeRows(rows = []) {
+ return Array.isArray(rows)
+ ? rows
+ .map((row) => ({
+ deviceSite: resolveStationValue(row?.deviceSite || row?.deviceSiteName || ''),
+ site: resolveStationValue(row?.site || row?.siteName || ''),
+ target: String(row?.target || '').trim()
+ }))
+ .filter((row) => row.deviceSite || row.site || row.target)
+ : []
+ }
+
+ function loadFormData() {
+ const resolved = buildDeviceSiteInitModel(props.initialData)
+ Object.assign(form, {
+ ...resolved,
+ rows: normalizeRows(resolved.rows.length ? resolved.rows : [{ deviceSite: '', site: '', target: '' }])
+ })
+ }
+
+ function resetForm() {
+ Object.assign(form, createDeviceSiteInitState())
+ form.rows = [{ deviceSite: '', site: '', target: '' }]
+ formRef.value?.clearValidate?.()
+ }
+
+ function addRow() {
+ form.rows.push({ deviceSite: '', site: '', target: '' })
+ }
+
+ function removeRow(index) {
+ if (form.rows.length <= 1) {
+ return
+ }
+ form.rows.splice(index, 1)
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ const validRows = normalizeRows(form.rows)
+ if (!validRows.length) {
+ ElMessage.error('璇疯嚦灏戝~鍐欎竴琛屽畬鏁寸殑璺緞琛�')
+ return
+ }
+ emit('submit', { ...form, rows: validRows })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ async (visible) => {
+ if (visible) {
+ loadFormData()
+ await nextTick()
+ formRef.value?.clearValidate?.()
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.initialData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+
+ watch(
+ () => props.stationOptions,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat-rela/index.vue b/rsf-design/src/views/basic-info/loc-area-mat-rela/index.vue
new file mode 100644
index 0000000..7194e06
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat-rela/index.vue
@@ -0,0 +1,471 @@
+<template>
+ <div class="loc-area-mat-rela-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板搴撳尯鐗╂枡鍏崇郴</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <LocAreaMatRelaDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :rela-data="currentRelaData"
+ :area-mat-options="areaMatOptions"
+ :area-options="areaOptions"
+ :group-options="groupOptions"
+ :matnr-options="matnrOptions"
+ :loc-type-options="locTypeOptions"
+ :loc-options="locOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <LocAreaMatRelaDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchWarehouseAreasList } from '@/api/loc'
+ import { fetchLocAreaMatList } from '@/api/loc-area-mat'
+ import { fetchLocPage, fetchLocTypeList } from '@/api/loc'
+ import { fetchMatnrGroupTree, fetchMatnrPage } from '@/api/wh-mat'
+ import {
+ fetchDeleteLocAreaMatRela,
+ fetchExportLocAreaMatRelaReport,
+ fetchGetLocAreaMatRelaDetail,
+ fetchGetLocAreaMatRelaMany,
+ fetchLocAreaMatRelaPage,
+ fetchSaveLocAreaMatRela,
+ fetchUpdateLocAreaMatRela
+ } from '@/api/loc-area-mat-rela'
+ import LocAreaMatRelaDialog from './modules/loc-area-mat-rela-dialog.vue'
+ import LocAreaMatRelaDetailDrawer from './modules/loc-area-mat-rela-detail-drawer.vue'
+ import { createLocAreaMatRelaTableColumns } from './locAreaMatRelaTable.columns'
+ import {
+ LOC_AREA_MAT_RELA_REPORT_STYLE,
+ LOC_AREA_MAT_RELA_REPORT_TITLE,
+ buildLocAreaMatRelaDialogModel,
+ buildLocAreaMatRelaPageQueryParams,
+ buildLocAreaMatRelaPrintRows,
+ buildLocAreaMatRelaReportMeta,
+ buildLocAreaMatRelaSavePayload,
+ buildLocAreaMatRelaSearchParams,
+ createLocAreaMatRelaLookupMaps,
+ createLocAreaMatRelaSearchState,
+ getLocAreaMatRelaPaginationKey,
+ getLocAreaMatRelaStatusOptions,
+ normalizeLocAreaMatRelaDetailRecord,
+ normalizeLocAreaMatRelaListRow,
+ resolveLocAreaMatRelaAreaMatOptions,
+ resolveLocAreaMatRelaAreaOptions,
+ resolveLocAreaMatRelaGroupOptions,
+ resolveLocAreaMatRelaLocOptions,
+ resolveLocAreaMatRelaLocTypeOptions,
+ resolveLocAreaMatRelaMatnrOptions
+ } from './locAreaMatRelaPage.helpers'
+
+ defineOptions({ name: 'LocAreaMatRela' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createLocAreaMatRelaSearchState())
+ const areaMatOptions = ref([])
+ const areaOptions = ref([])
+ const groupOptions = ref([])
+ const matnrOptions = ref([])
+ const locTypeOptions = ref([])
+ const locOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = LOC_AREA_MAT_RELA_REPORT_TITLE
+ const reportQueryParams = computed(() => buildLocAreaMatRelaSearchParams(searchForm.value))
+ const lookupMaps = computed(() =>
+ createLocAreaMatRelaLookupMaps({
+ areaMatOptions: areaMatOptions.value,
+ areaOptions: areaOptions.value,
+ matnrOptions: matnrOptions.value,
+ groupOptions: groupOptions.value,
+ locTypeOptions: locTypeOptions.value,
+ locOptions: locOptions.value
+ })
+ )
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鍙�/澶囨敞/鍏崇郴淇℃伅'
+ }
+ },
+ {
+ label: '涓诲崟',
+ key: 'areaMatId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: areaMatOptions.value
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: areaOptions.value
+ }
+ },
+ {
+ label: '缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鍙�'
+ }
+ },
+ {
+ label: '鐗╂枡',
+ key: 'matnrId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: matnrOptions.value
+ }
+ },
+ {
+ label: '鐗╂枡鍒嗙粍',
+ key: 'groupId',
+ type: 'treeselect',
+ props: {
+ data: groupOptions.value,
+ props: {
+ label: 'displayLabel',
+ value: 'id',
+ children: 'children'
+ },
+ clearable: true,
+ checkStrictly: true,
+ defaultExpandAll: true,
+ placeholder: '璇烽�夋嫨鐗╂枡鍒嗙粍'
+ }
+ },
+ {
+ label: '搴撲綅绫诲瀷',
+ key: 'locTypeId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: locTypeOptions.value
+ }
+ },
+ {
+ label: '搴撲綅',
+ key: 'locId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: locOptions.value
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getLocAreaMatRelaStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocAreaMatRelaDetail(row.id), {}, {
+ timeoutMessage: '搴撳尯鐗╂枡鍏崇郴璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeLocAreaMatRelaDetailRecord(detail, lookupMaps.value)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撳尯鐗╂枡鍏崇郴璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocAreaMatRelaDetail(row.id), {}, {
+ timeoutMessage: '搴撳尯鐗╂枡鍏崇郴璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇搴撳尯鐗╂枡鍏崇郴璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchLocAreaMatRelaPage,
+ apiParams: buildLocAreaMatRelaPageQueryParams(searchForm.value),
+ paginationKey: getLocAreaMatRelaPaginationKey(),
+ columnsFactory: () =>
+ createLocAreaMatRelaTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeLocAreaMatRelaListRow(item, lookupMaps.value))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentRelaData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildLocAreaMatRelaDialogModel(),
+ buildEditModel: (record) => buildLocAreaMatRelaDialogModel(record),
+ buildSavePayload: (formData) => buildLocAreaMatRelaSavePayload(formData),
+ saveRequest: fetchSaveLocAreaMatRela,
+ updateRequest: fetchUpdateLocAreaMatRela,
+ deleteRequest: fetchDeleteLocAreaMatRela,
+ entityName: '搴撳尯鐗╂枡鍏崇郴',
+ resolveRecordLabel: (record) => record?.areaMatIdText || record?.code || record?.relationTypeText || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewDialogMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ const response =
+ Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchGetLocAreaMatRelaMany(payload.ids)
+ : await fetchLocAreaMatRelaPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'loc-area-mat-rela.xlsx',
+ requestExport: (payload) =>
+ fetchExportLocAreaMatRelaReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildLocAreaMatRelaPrintRows(records, lookupMaps.value),
+ buildPreviewMeta: buildPreviewDialogMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildLocAreaMatRelaReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || LOC_AREA_MAT_RELA_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildLocAreaMatRelaSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createLocAreaMatRelaSearchState())
+ resetSearchParams()
+ }
+
+ async function loadAreaMatOptions() {
+ const records = await guardRequestWithMessage(fetchLocAreaMatList(), [], {
+ timeoutMessage: '涓诲崟閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaMatOptions.value = resolveLocAreaMatRelaAreaMatOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadAreaOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveLocAreaMatRelaAreaOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadGroupOptions() {
+ const records = await guardRequestWithMessage(fetchMatnrGroupTree({}), [], {
+ timeoutMessage: '鐗╂枡鍒嗙粍閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ groupOptions.value = resolveLocAreaMatRelaGroupOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadMatnrOptions() {
+ const response = await guardRequestWithMessage(
+ fetchMatnrPage({ current: 1, pageSize: 200 }),
+ { records: [] },
+ { timeoutMessage: '鐗╂枡閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ matnrOptions.value = resolveLocAreaMatRelaMatnrOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadLocTypeOptions() {
+ const records = await guardRequestWithMessage(fetchLocTypeList(), [], {
+ timeoutMessage: '搴撲綅绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ locTypeOptions.value = resolveLocAreaMatRelaLocTypeOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadLocOptions() {
+ const response = await guardRequestWithMessage(
+ fetchLocPage({ current: 1, pageSize: 200 }),
+ { records: [] },
+ { timeoutMessage: '搴撲綅閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ locOptions.value = resolveLocAreaMatRelaLocOptions(defaultResponseAdapter(response).records)
+ }
+
+ onMounted(async () => {
+ await Promise.all([
+ loadAreaMatOptions(),
+ loadAreaOptions(),
+ loadGroupOptions(),
+ loadMatnrOptions(),
+ loadLocTypeOptions(),
+ loadLocOptions(),
+ getData()
+ ])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat-rela/locAreaMatRelaPage.helpers.js b/rsf-design/src/views/basic-info/loc-area-mat-rela/locAreaMatRelaPage.helpers.js
new file mode 100644
index 0000000..7a26c7f
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat-rela/locAreaMatRelaPage.helpers.js
@@ -0,0 +1,389 @@
+import {
+ normalizeLocAreaMatGroupTreeOptions,
+ resolveLocAreaMatAreaOptions,
+ resolveLocAreaMatLocOptions,
+ resolveLocAreaMatLocTypeOptions,
+ resolveLocAreaMatMatnrOptions
+} from '../loc-area-mat/locAreaMatPage.helpers.js'
+
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const LOC_AREA_MAT_RELA_REPORT_TITLE = '搴撳尯鐗╂枡鍏崇郴鎶ヨ〃'
+export const LOC_AREA_MAT_RELA_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeOptionId(item = {}) {
+ const value = item.id ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return void 0
+ }
+ return normalizeNumber(value)
+}
+
+function normalizeOptionText(item = {}, fallbackPrefix = '') {
+ return (
+ normalizeText(
+ item.displayLabel ||
+ item.name ||
+ item.code ||
+ item.label ||
+ item.codeText ||
+ `${fallbackPrefix}${item.id ?? item.value ?? ''}`
+ ) || '--'
+ )
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+function createLookupMap(options = [], childrenKey = 'children') {
+ const map = {}
+ const walk = (items = []) => {
+ items.forEach((item) => {
+ if (!item || typeof item !== 'object') return
+ const value = normalizeOptionId(item)
+ if (value !== void 0) {
+ map[String(value)] = normalizeOptionText(item)
+ }
+ const children = item[childrenKey]
+ if (Array.isArray(children) && children.length > 0) {
+ walk(children)
+ }
+ })
+ }
+ walk(Array.isArray(options) ? options : [])
+ return map
+}
+
+function resolveMapLabel(record, field, map, fallbackKeys = []) {
+ const rawValue = record?.[field]
+ const candidates = [
+ record?.[`${field}$`],
+ record?.[`${field}Text`],
+ ...fallbackKeys.map((key) => record?.[key])
+ ]
+ const mapped = rawValue !== void 0 && rawValue !== null && rawValue !== '' ? map[String(rawValue)] : void 0
+ return normalizeText(candidates.find((item) => normalizeText(item)) || mapped || '')
+}
+
+function resolveRelationTypeText(record = {}) {
+ const hasMatnr = record.matnrId !== void 0 || record.matnrId$ !== void 0 || record.matnrIdText !== void 0
+ const hasLoc = record.locId !== void 0 || record.locId$ !== void 0 || record.locIdText !== void 0
+ if (hasMatnr && hasLoc) return '娣峰悎鍏崇郴'
+ if (hasMatnr) return '鐗╂枡鍏崇郴'
+ if (hasLoc) return '搴撲綅鍏崇郴'
+ return '鏈垎绫�'
+}
+
+export function createLocAreaMatRelaSearchState() {
+ return {
+ condition: '',
+ areaMatId: '',
+ areaId: '',
+ code: '',
+ matnrId: '',
+ groupId: '',
+ locTypeId: '',
+ locId: '',
+ status: '',
+ memo: ''
+ }
+}
+
+export function createLocAreaMatRelaFormState() {
+ return {
+ id: void 0,
+ areaMatId: '',
+ areaId: '',
+ code: '',
+ matnrId: '',
+ groupId: '',
+ locTypeId: '',
+ locId: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getLocAreaMatRelaStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getLocAreaMatRelaStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function getLocAreaMatRelaPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildLocAreaMatRelaSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ areaMatId:
+ params.areaMatId !== undefined && params.areaMatId !== null && params.areaMatId !== ''
+ ? normalizeNumber(params.areaMatId)
+ : void 0,
+ areaId:
+ params.areaId !== undefined && params.areaId !== null && params.areaId !== ''
+ ? normalizeNumber(params.areaId)
+ : void 0,
+ code: normalizeText(params.code),
+ matnrId:
+ params.matnrId !== undefined && params.matnrId !== null && params.matnrId !== ''
+ ? normalizeNumber(params.matnrId)
+ : void 0,
+ groupId:
+ params.groupId !== undefined && params.groupId !== null && params.groupId !== ''
+ ? normalizeNumber(params.groupId)
+ : void 0,
+ locTypeId:
+ params.locTypeId !== undefined && params.locTypeId !== null && params.locTypeId !== ''
+ ? normalizeNumber(params.locTypeId)
+ : void 0,
+ locId:
+ params.locId !== undefined && params.locId !== null && params.locId !== ''
+ ? normalizeNumber(params.locId)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0,
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocAreaMatRelaPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildLocAreaMatRelaSearchParams(params)
+ }
+}
+
+export function buildLocAreaMatRelaSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: normalizeNumber(formData.id) }
+ : {}),
+ ...(formData.areaMatId !== void 0 && formData.areaMatId !== null && formData.areaMatId !== ''
+ ? { areaMatId: normalizeNumber(formData.areaMatId) }
+ : {}),
+ ...(formData.areaId !== void 0 && formData.areaId !== null && formData.areaId !== ''
+ ? { areaId: normalizeNumber(formData.areaId) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ ...(formData.matnrId !== void 0 && formData.matnrId !== null && formData.matnrId !== ''
+ ? { matnrId: normalizeNumber(formData.matnrId) }
+ : {}),
+ ...(formData.groupId !== void 0 && formData.groupId !== null && formData.groupId !== ''
+ ? { groupId: normalizeNumber(formData.groupId) }
+ : {}),
+ ...(formData.locTypeId !== void 0 && formData.locTypeId !== null && formData.locTypeId !== ''
+ ? { locTypeId: normalizeNumber(formData.locTypeId) }
+ : {}),
+ ...(formData.locId !== void 0 && formData.locId !== null && formData.locId !== ''
+ ? { locId: normalizeNumber(formData.locId) }
+ : {}),
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? normalizeNumber(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildLocAreaMatRelaDialogModel(record = {}) {
+ return {
+ ...createLocAreaMatRelaFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: normalizeNumber(record.id) } : {}),
+ areaMatId:
+ record.areaMatId !== undefined && record.areaMatId !== null && record.areaMatId !== ''
+ ? normalizeNumber(record.areaMatId)
+ : '',
+ areaId:
+ record.areaId !== undefined && record.areaId !== null && record.areaId !== ''
+ ? normalizeNumber(record.areaId)
+ : '',
+ code: normalizeText(record.code || ''),
+ matnrId:
+ record.matnrId !== undefined && record.matnrId !== null && record.matnrId !== ''
+ ? normalizeNumber(record.matnrId)
+ : '',
+ groupId:
+ record.groupId !== undefined && record.groupId !== null && record.groupId !== ''
+ ? normalizeNumber(record.groupId)
+ : '',
+ locTypeId:
+ record.locTypeId !== undefined && record.locTypeId !== null && record.locTypeId !== ''
+ ? normalizeNumber(record.locTypeId)
+ : '',
+ locId:
+ record.locId !== undefined && record.locId !== null && record.locId !== ''
+ ? normalizeNumber(record.locId)
+ : '',
+ status: record.status !== undefined && record.status !== null ? normalizeNumber(record.status, 1) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeLocAreaMatRelaListRow(record = {}, lookups = {}) {
+ const areaMatMap = lookups.areaMatMap || createLookupMap(lookups.areaMatOptions || [])
+ const areaMap = lookups.areaMap || createLookupMap(lookups.areaOptions || [])
+ const matnrMap = lookups.matnrMap || createLookupMap(lookups.matnrOptions || [])
+ const groupMap = lookups.groupMap || createLookupMap(lookups.groupOptions || [])
+ const locTypeMap = lookups.locTypeMap || createLookupMap(lookups.locTypeOptions || [])
+ const locMap = lookups.locMap || createLookupMap(lookups.locOptions || [])
+
+ const statusMeta = getLocAreaMatRelaStatusMeta(record.statusBool ?? record.status)
+
+ return {
+ ...record,
+ areaMatIdText: resolveMapLabel(record, 'areaMatId', areaMatMap, ['areaMatCode', 'areaMatName']),
+ areaIdText: resolveMapLabel(record, 'areaId', areaMap, ['areaName']),
+ code: normalizeText(record.code || ''),
+ matnrIdText: resolveMapLabel(record, 'matnrId', matnrMap, ['matnrCode', 'matnrName']),
+ groupIdText: resolveMapLabel(record, 'groupId', groupMap, ['groupName']),
+ locTypeIdText: resolveMapLabel(record, 'locTypeId', locTypeMap, ['locTypeName', 'typeName']),
+ locIdText: resolveMapLabel(record, 'locId', locMap, ['locCode', 'locName']),
+ relationTypeText: resolveRelationTypeText(record),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeLocAreaMatRelaDetailRecord(record = {}, lookups = {}) {
+ return normalizeLocAreaMatRelaListRow(record, lookups)
+}
+
+export function buildLocAreaMatRelaPrintRows(records = [], lookups = {}) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeLocAreaMatRelaListRow(record, lookups))
+}
+
+export function buildLocAreaMatRelaReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = LOC_AREA_MAT_RELA_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: LOC_AREA_MAT_RELA_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...LOC_AREA_MAT_RELA_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function createLocAreaMatRelaLookupMaps({
+ areaMatOptions = [],
+ areaOptions = [],
+ matnrOptions = [],
+ groupOptions = [],
+ locTypeOptions = [],
+ locOptions = []
+} = {}) {
+ return {
+ areaMatMap: createLookupMap(areaMatOptions),
+ areaMap: createLookupMap(areaOptions),
+ matnrMap: createLookupMap(matnrOptions),
+ groupMap: createLookupMap(groupOptions),
+ locTypeMap: createLookupMap(locTypeOptions),
+ locMap: createLookupMap(locOptions)
+ }
+}
+
+export function resolveLocAreaMatRelaAreaMatOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records
+ .map((item) => {
+ const value = normalizeOptionId(item)
+ if (value === void 0) return null
+ const label =
+ [
+ normalizeText(item.code || item.areaMatCode || ''),
+ normalizeText(item.depict || item.areaMatName || ''),
+ normalizeText(item.warehouseName || item.warehouseId$ || ''),
+ normalizeText(item.areaName || item.areaId$ || '')
+ ]
+ .filter(Boolean)
+ .join(' 路 ') || `涓诲崟 ${value}`
+ return {
+ value,
+ label,
+ areaId:
+ item.areaId !== undefined && item.areaId !== null && item.areaId !== ''
+ ? normalizeNumber(item.areaId)
+ : void 0
+ }
+ })
+ .filter(Boolean)
+}
+
+export {
+ normalizeLocAreaMatGroupTreeOptions as resolveLocAreaMatRelaGroupOptions,
+ resolveLocAreaMatAreaOptions as resolveLocAreaMatRelaAreaOptions,
+ resolveLocAreaMatLocOptions as resolveLocAreaMatRelaLocOptions,
+ resolveLocAreaMatLocTypeOptions as resolveLocAreaMatRelaLocTypeOptions,
+ resolveLocAreaMatMatnrOptions as resolveLocAreaMatRelaMatnrOptions
+}
diff --git a/rsf-design/src/views/basic-info/loc-area-mat-rela/locAreaMatRelaTable.columns.js b/rsf-design/src/views/basic-info/loc-area-mat-rela/locAreaMatRelaTable.columns.js
new file mode 100644
index 0000000..f0f2348
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat-rela/locAreaMatRelaTable.columns.js
@@ -0,0 +1,138 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getLocAreaMatRelaStatusMeta } from './locAreaMatRelaPage.helpers'
+
+export function createLocAreaMatRelaTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'areaMatIdText',
+ label: '涓诲崟',
+ minWidth: 200,
+ showOverflowTooltip: true,
+ formatter: (row) => row.areaMatIdText || row.areaMatId || '--'
+ },
+ {
+ prop: 'areaIdText',
+ label: '搴撳尯',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.areaIdText || '--'
+ },
+ {
+ prop: 'code',
+ label: '缂栧彿',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'matnrIdText',
+ label: '鐗╂枡',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrIdText || '--'
+ },
+ {
+ prop: 'groupIdText',
+ label: '鐗╂枡鍒嗙粍',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.groupIdText || '--'
+ },
+ {
+ prop: 'locTypeIdText',
+ label: '搴撲綅绫诲瀷',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locTypeIdText || '--'
+ },
+ {
+ prop: 'locIdText',
+ label: '搴撲綅',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locIdText || '--'
+ },
+ {
+ prop: 'relationTypeText',
+ label: '鍏崇郴绫诲瀷',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.relationTypeText || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getLocAreaMatRelaStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/loc-area-mat-rela/modules/loc-area-mat-rela-detail-drawer.vue b/rsf-design/src/views/basic-info/loc-area-mat-rela/modules/loc-area-mat-rela-detail-drawer.vue
new file mode 100644
index 0000000..43bc37f
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat-rela/modules/loc-area-mat-rela-detail-drawer.vue
@@ -0,0 +1,60 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳尯鐗╂枡鍏崇郴璇︽儏"
+ size="1120px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍏崇郴淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="涓诲崟">{{ detail.areaMatIdText || detail.areaMatId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯">{{ detail.areaIdText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缂栧彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡">{{ detail.matnrIdText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍒嗙粍">{{ detail.groupIdText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅绫诲瀷">{{ detail.locTypeIdText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅">{{ detail.locIdText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat-rela/modules/loc-area-mat-rela-dialog.vue b/rsf-design/src/views/basic-info/loc-area-mat-rela/modules/loc-area-mat-rela-dialog.vue
new file mode 100644
index 0000000..73cdad1
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat-rela/modules/loc-area-mat-rela-dialog.vue
@@ -0,0 +1,261 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="980px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocAreaMatRelaDialogModel,
+ createLocAreaMatRelaFormState
+ } from '../locAreaMatRelaPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ relaData: { type: Object, default: () => ({}) },
+ areaMatOptions: { type: Array, default: () => [] },
+ areaOptions: { type: Array, default: () => [] },
+ groupOptions: { type: Array, default: () => [] },
+ matnrOptions: { type: Array, default: () => [] },
+ locTypeOptions: { type: Array, default: () => [] },
+ locOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+
+ const formRef = ref()
+ const form = reactive(createLocAreaMatRelaFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫搴撳尯鐗╂枡鍏崇郴' : '鏂板搴撳尯鐗╂枡鍏崇郴'))
+
+ const selectedAreaId = computed(() =>
+ form.areaId !== void 0 && form.areaId !== null && form.areaId !== ''
+ ? Number(form.areaId)
+ : void 0
+ )
+ const selectedGroupId = computed(() =>
+ form.groupId !== void 0 && form.groupId !== null && form.groupId !== ''
+ ? Number(form.groupId)
+ : void 0
+ )
+
+ const filteredAreaMatOptions = computed(() => {
+ if (selectedAreaId.value === void 0) {
+ return props.areaMatOptions
+ }
+ return props.areaMatOptions.filter(
+ (item) => item.areaId === void 0 || Number(item.areaId) === selectedAreaId.value
+ )
+ })
+
+ const filteredMatnrOptions = computed(() => {
+ if (selectedGroupId.value === void 0) {
+ return props.matnrOptions
+ }
+ return props.matnrOptions.filter(
+ (item) => item.groupId === void 0 || Number(item.groupId) === selectedGroupId.value
+ )
+ })
+
+ const filteredLocOptions = computed(() => {
+ if (selectedAreaId.value === void 0) {
+ return props.locOptions
+ }
+ return props.locOptions.filter(
+ (item) => item.areaId === void 0 || Number(item.areaId) === selectedAreaId.value
+ )
+ })
+
+ const rules = computed(() => ({
+ areaMatId: [{ required: true, message: '璇烽�夋嫨涓诲崟', trigger: 'change' }],
+ areaId: [{ required: true, message: '璇烽�夋嫨搴撳尯', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '涓诲崟',
+ key: 'areaMatId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨涓诲崟',
+ clearable: true,
+ filterable: true,
+ options: filteredAreaMatOptions.value
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撳尯',
+ clearable: true,
+ filterable: true,
+ options: props.areaOptions
+ }
+ },
+ {
+ label: '缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ紪鍙�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐗╂枡',
+ key: 'matnrId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐗╂枡',
+ clearable: true,
+ filterable: true,
+ options: filteredMatnrOptions.value
+ }
+ },
+ {
+ label: '鐗╂枡鍒嗙粍',
+ key: 'groupId',
+ type: 'treeselect',
+ props: {
+ data: props.groupOptions,
+ props: {
+ label: 'displayLabel',
+ value: 'id',
+ children: 'children'
+ },
+ placeholder: '璇烽�夋嫨鐗╂枡鍒嗙粍',
+ clearable: true,
+ checkStrictly: true,
+ defaultExpandAll: true
+ }
+ },
+ {
+ label: '搴撲綅绫诲瀷',
+ key: 'locTypeId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撲綅绫诲瀷',
+ clearable: true,
+ filterable: true,
+ options: props.locTypeOptions
+ }
+ },
+ {
+ label: '搴撲綅',
+ key: 'locId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撲綅',
+ clearable: true,
+ filterable: true,
+ options: filteredLocOptions.value
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildLocAreaMatRelaDialogModel(props.relaData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createLocAreaMatRelaFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.relaData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat/index.vue b/rsf-design/src/views/basic-info/loc-area-mat/index.vue
new file mode 100644
index 0000000..d81b1d6
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat/index.vue
@@ -0,0 +1,365 @@
+<template>
+ <div class="loc-area-mat-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板搴撳尯鐗╂枡</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <LocAreaMatDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :loc-area-mat-data="currentLocAreaMatData"
+ :warehouse-options="warehouseOptions"
+ :area-options="areaOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <LocAreaMatDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchWarehouseAreasList, fetchWarehouseList } from '@/api/loc'
+ import {
+ fetchDeleteLocAreaMat,
+ fetchExportLocAreaMatReport,
+ fetchGetLocAreaMatDetail,
+ fetchGetLocAreaMatMany,
+ fetchLocAreaMatPage,
+ fetchSaveLocAreaMat,
+ fetchUpdateLocAreaMat
+ } from '@/api/loc-area-mat'
+ import LocAreaMatDialog from './modules/loc-area-mat-dialog.vue'
+ import LocAreaMatDetailDrawer from './modules/loc-area-mat-detail-drawer.vue'
+ import { createLocAreaMatTableColumns } from './locAreaMatTable.columns'
+ import {
+ LOC_AREA_MAT_REPORT_STYLE,
+ LOC_AREA_MAT_REPORT_TITLE,
+ buildLocAreaMatDialogModel,
+ buildLocAreaMatPageQueryParams,
+ buildLocAreaMatPrintRows,
+ buildLocAreaMatReportMeta,
+ buildLocAreaMatSavePayload,
+ buildLocAreaMatSearchParams,
+ createLocAreaMatSearchState,
+ getLocAreaMatPaginationKey,
+ getLocAreaMatStatusOptions,
+ normalizeLocAreaMatListRow,
+ resolveLocAreaMatAreaOptions,
+ resolveLocAreaMatWarehouseOptions
+ } from './locAreaMatPage.helpers'
+
+ defineOptions({ name: 'LocAreaMat' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createLocAreaMatSearchState())
+ const warehouseOptions = ref([])
+ const areaOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = LOC_AREA_MAT_REPORT_TITLE
+ const reportQueryParams = computed(() => buildLocAreaMatSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ�昏緫缂栧彿/鎻忚堪/澶囨敞'
+ }
+ },
+ {
+ label: '閫昏緫缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ�昏緫缂栧彿'
+ }
+ },
+ {
+ label: '浠撳簱',
+ key: 'warehouseId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: warehouseOptions.value
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: areaOptions.value
+ }
+ },
+ {
+ label: '閫昏緫鎻忚堪',
+ key: 'depict',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ�昏緫鎻忚堪'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getLocAreaMatStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocAreaMatDetail(row.id), {}, {
+ timeoutMessage: '搴撳尯鐗╂枡璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeLocAreaMatListRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撳尯鐗╂枡璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocAreaMatDetail(row.id), {}, {
+ timeoutMessage: '搴撳尯鐗╂枡璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇搴撳尯鐗╂枡璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchLocAreaMatPage,
+ apiParams: buildLocAreaMatPageQueryParams(searchForm.value),
+ paginationKey: getLocAreaMatPaginationKey(),
+ columnsFactory: () =>
+ createLocAreaMatTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeLocAreaMatListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentLocAreaMatData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildLocAreaMatDialogModel(),
+ buildEditModel: (record) => buildLocAreaMatDialogModel(record),
+ buildSavePayload: (formData) => buildLocAreaMatSavePayload(formData),
+ saveRequest: fetchSaveLocAreaMat,
+ updateRequest: fetchUpdateLocAreaMat,
+ deleteRequest: fetchDeleteLocAreaMat,
+ entityName: '搴撳尯鐗╂枡',
+ resolveRecordLabel: (record) => record?.code || record?.depict || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewDialogMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ const response = Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchGetLocAreaMatMany(payload.ids)
+ : await fetchLocAreaMatPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'loc-area-mat.xlsx',
+ requestExport: (payload) =>
+ fetchExportLocAreaMatReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildLocAreaMatPrintRows(records),
+ buildPreviewMeta: buildPreviewDialogMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildLocAreaMatReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || LOC_AREA_MAT_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildLocAreaMatSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createLocAreaMatSearchState())
+ resetSearchParams()
+ }
+
+ async function loadWarehouseOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseList(), [], {
+ timeoutMessage: '浠撳簱閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ warehouseOptions.value = resolveLocAreaMatWarehouseOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadAreaOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveLocAreaMatAreaOptions(defaultResponseAdapter(records).records)
+ }
+
+ onMounted(async () => {
+ await Promise.all([loadWarehouseOptions(), loadAreaOptions(), getData()])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat/locAreaMatPage.helpers.js b/rsf-design/src/views/basic-info/loc-area-mat/locAreaMatPage.helpers.js
new file mode 100644
index 0000000..83ebf93
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat/locAreaMatPage.helpers.js
@@ -0,0 +1,449 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const LOC_AREA_MAT_REPORT_TITLE = '搴撳尯鐗╂枡鎶ヨ〃'
+export const LOC_AREA_MAT_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+export function createLocAreaMatSearchState() {
+ return {
+ condition: '',
+ code: '',
+ warehouseId: '',
+ areaId: '',
+ depict: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createLocAreaMatFormState() {
+ return {
+ id: void 0,
+ code: '',
+ warehouseId: '',
+ areaId: '',
+ depict: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function createLocAreaMatRelationBindMatnrState(areaMatId = void 0) {
+ return {
+ areaMatId,
+ warehouseId: '',
+ areaId: '',
+ groupId: '',
+ matnrId: [],
+ typeId: []
+ }
+}
+
+export function createLocAreaMatRelationBindLocState(areaMatId = void 0) {
+ return {
+ areaMatId,
+ warehouseId: '',
+ areaId: '',
+ groupId: '',
+ locId: [],
+ typeId: []
+ }
+}
+
+export function buildLocAreaMatRelationBindLocState(areaMatId = void 0) {
+ return createLocAreaMatRelationBindLocState(areaMatId)
+}
+
+export function buildLocAreaMatRelationBindMatnrModel(record = {}) {
+ return {
+ ...createLocAreaMatRelationBindMatnrState(record.areaMatId),
+ ...(record.areaMatId !== void 0 && record.areaMatId !== null && record.areaMatId !== ''
+ ? { areaMatId: normalizeNumber(record.areaMatId) }
+ : {}),
+ warehouseId:
+ record.warehouseId !== undefined && record.warehouseId !== null && record.warehouseId !== ''
+ ? normalizeNumber(record.warehouseId)
+ : '',
+ areaId:
+ record.areaId !== undefined && record.areaId !== null && record.areaId !== ''
+ ? normalizeNumber(record.areaId)
+ : '',
+ groupId:
+ record.groupId !== undefined && record.groupId !== null && record.groupId !== ''
+ ? normalizeNumber(record.groupId)
+ : '',
+ matnrId: Array.isArray(record.matnrId) ? record.matnrId.map((item) => normalizeNumber(item)).filter(Boolean) : [],
+ typeId: Array.isArray(record.typeId) ? record.typeId.map((item) => normalizeNumber(item)).filter(Boolean) : []
+ }
+}
+
+export function buildLocAreaMatRelationBindLocModel(record = {}) {
+ return {
+ ...createLocAreaMatRelationBindLocState(record.areaMatId),
+ ...(record.areaMatId !== void 0 && record.areaMatId !== null && record.areaMatId !== ''
+ ? { areaMatId: normalizeNumber(record.areaMatId) }
+ : {}),
+ warehouseId:
+ record.warehouseId !== undefined && record.warehouseId !== null && record.warehouseId !== ''
+ ? normalizeNumber(record.warehouseId)
+ : '',
+ areaId:
+ record.areaId !== undefined && record.areaId !== null && record.areaId !== ''
+ ? normalizeNumber(record.areaId)
+ : '',
+ groupId:
+ record.groupId !== undefined && record.groupId !== null && record.groupId !== ''
+ ? normalizeNumber(record.groupId)
+ : '',
+ locId: Array.isArray(record.locId) ? record.locId.map((item) => normalizeNumber(item)).filter(Boolean) : [],
+ typeId: Array.isArray(record.typeId) ? record.typeId.map((item) => normalizeNumber(item)).filter(Boolean) : []
+ }
+}
+
+export function getLocAreaMatStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getLocAreaMatPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getLocAreaMatStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildLocAreaMatSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ warehouseId:
+ params.warehouseId !== undefined && params.warehouseId !== null && params.warehouseId !== ''
+ ? normalizeNumber(params.warehouseId)
+ : void 0,
+ areaId:
+ params.areaId !== undefined && params.areaId !== null && params.areaId !== ''
+ ? normalizeNumber(params.areaId)
+ : void 0,
+ depict: normalizeText(params.depict),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocAreaMatPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildLocAreaMatSearchParams(params)
+ }
+}
+
+export function buildLocAreaMatSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: normalizeNumber(formData.id) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ warehouseId:
+ formData.warehouseId !== undefined && formData.warehouseId !== null && formData.warehouseId !== ''
+ ? normalizeNumber(formData.warehouseId)
+ : void 0,
+ areaId:
+ formData.areaId !== undefined && formData.areaId !== null && formData.areaId !== ''
+ ? normalizeNumber(formData.areaId)
+ : void 0,
+ depict: normalizeText(formData.depict) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? normalizeNumber(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildLocAreaMatDialogModel(record = {}) {
+ return {
+ ...createLocAreaMatFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: normalizeNumber(record.id) } : {}),
+ code: normalizeText(record.code || ''),
+ warehouseId:
+ record.warehouseId !== undefined && record.warehouseId !== null && record.warehouseId !== ''
+ ? normalizeNumber(record.warehouseId)
+ : '',
+ areaId:
+ record.areaId !== undefined && record.areaId !== null && record.areaId !== ''
+ ? normalizeNumber(record.areaId)
+ : '',
+ depict: normalizeText(record.depict || ''),
+ status: record.status !== void 0 && record.status !== null ? normalizeNumber(record.status, 1) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeLocAreaMatListRow(record = {}) {
+ const statusMeta = getLocAreaMatStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ code: normalizeText(record.code || ''),
+ depict: normalizeText(record.depict || ''),
+ warehouseName: normalizeText(record.warehouseId$ || record.warehouseName || ''),
+ areaName: normalizeText(record.areaId$ || record.areaName || ''),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ warehouseIdText: normalizeText(record.warehouseId$ || record.warehouseIdText || ''),
+ areaIdText: normalizeText(record.areaId$ || record.areaIdText || '')
+ }
+}
+
+export function normalizeLocAreaMatDetailRecord(record = {}) {
+ return normalizeLocAreaMatListRow(record)
+}
+
+export function normalizeLocAreaMatRelationRow(record = {}) {
+ const relationTypeText =
+ record.matnrId !== void 0 ||
+ record.matnrId$ !== void 0 ||
+ record.typeId !== void 0 ||
+ record.typeId$ !== void 0
+ ? '鐗╂枡缁戝畾'
+ : '搴撲綅缁戝畾'
+
+ return {
+ ...record,
+ warehouseIdText: normalizeText(record.warehouseId$ || record.warehouseIdText || ''),
+ areaIdText: normalizeText(record.areaId$ || record.areaIdText || ''),
+ matnrIdText: normalizeText(record.matnrId$ || record.matnrIdText || record.matnrCode || record.matnrName || ''),
+ groupIdText: normalizeText(record.groupId$ || record.groupIdText || record.groupName || ''),
+ locTypeIdText: normalizeText(record.locTypeId$ || record.locTypeIdText || record.typeName || record.typeText || ''),
+ locIdText: normalizeText(record.locId$ || record.locIdText || record.locCode || record.code || ''),
+ relationTypeText,
+ statusText: getLocAreaMatStatusMeta(record.statusBool ?? record.status).text,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function buildLocAreaMatPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeLocAreaMatListRow(record))
+}
+
+export function buildLocAreaMatReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = LOC_AREA_MAT_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: LOC_AREA_MAT_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...LOC_AREA_MAT_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+function normalizeOptionText(item = {}, fallbackPrefix = '') {
+ return normalizeText(item.name || item.code || item.label || item.codeText || `${fallbackPrefix}${item.id ?? item.value ?? ''}`) || '--'
+}
+
+function normalizeOptionId(item = {}) {
+ const value = item.id ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return void 0
+ }
+ return normalizeNumber(value)
+}
+
+export function resolveLocAreaMatWarehouseOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records
+ .map((item) => {
+ const value = normalizeOptionId(item)
+ if (value === void 0) return null
+ return {
+ value,
+ label: normalizeOptionText(item, '浠撳簱 ')
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveLocAreaMatAreaOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records
+ .map((item) => {
+ const value = normalizeOptionId(item)
+ if (value === void 0) return null
+ return {
+ value,
+ label: normalizeOptionText(item, '搴撳尯 '),
+ warehouseId:
+ item.warehouseId !== undefined && item.warehouseId !== null && item.warehouseId !== ''
+ ? normalizeNumber(item.warehouseId)
+ : void 0
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveLocAreaMatLocTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records
+ .map((item) => {
+ const value = normalizeOptionId(item)
+ if (value === void 0) return null
+ return {
+ value,
+ label: normalizeOptionText(item, '绫诲瀷 ')
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveLocAreaMatMatnrOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records
+ .map((item) => {
+ const value = normalizeOptionId(item)
+ if (value === void 0) return null
+ const label = normalizeText(item.name || item.code || item.maktx || item.barcode || `鐗╂枡 ${value}`)
+ return {
+ value,
+ label: label || `鐗╂枡 ${value}`,
+ groupId:
+ item.groupId !== undefined && item.groupId !== null && item.groupId !== ''
+ ? normalizeNumber(item.groupId)
+ : void 0
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveLocAreaMatLocOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records
+ .map((item) => {
+ const value = normalizeOptionId(item)
+ if (value === void 0) return null
+ const code = normalizeText(item.code || item.locCode || '')
+ const label = [code, normalizeText(item.warehouseName || item.areaName || '')].filter(Boolean).join(' 路 ') || `搴撲綅 ${value}`
+ return {
+ value,
+ label,
+ warehouseId:
+ item.warehouseId !== undefined && item.warehouseId !== null && item.warehouseId !== ''
+ ? normalizeNumber(item.warehouseId)
+ : void 0,
+ areaId:
+ item.areaId !== undefined && item.areaId !== null && item.areaId !== ''
+ ? normalizeNumber(item.areaId)
+ : void 0
+ }
+ })
+ .filter(Boolean)
+}
+
+export function normalizeLocAreaMatGroupTreeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ const walk = (nodes = []) =>
+ nodes
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = normalizeOptionId(item)
+ const children = walk(item.children || [])
+ if (value === void 0 && children.length === 0) {
+ return null
+ }
+ return {
+ id: value ?? 0,
+ name: normalizeText(item.name || '椤剁骇鍒嗙粍'),
+ code: normalizeText(item.code || ''),
+ displayLabel: [normalizeText(item.name || '椤剁骇鍒嗙粍'), normalizeText(item.code || '')].filter(Boolean).join(' 路 ') || '椤剁骇鍒嗙粍',
+ children
+ }
+ })
+ .filter(Boolean)
+
+ return [
+ {
+ id: 0,
+ name: '椤剁骇鍒嗙粍',
+ code: '',
+ displayLabel: '椤剁骇鍒嗙粍',
+ children: walk(records)
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/loc-area-mat/locAreaMatTable.columns.js b/rsf-design/src/views/basic-info/loc-area-mat/locAreaMatTable.columns.js
new file mode 100644
index 0000000..60c78ab
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat/locAreaMatTable.columns.js
@@ -0,0 +1,125 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getLocAreaMatStatusMeta } from './locAreaMatPage.helpers'
+
+export function createLocAreaMatTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'code',
+ label: '閫昏緫缂栧彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'warehouseName',
+ label: '浠撳簱',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.warehouseName || row.warehouseIdText || '--'
+ },
+ {
+ prop: 'areaName',
+ label: '搴撳尯',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.areaName || row.areaIdText || '--'
+ },
+ {
+ prop: 'depict',
+ label: '閫昏緫鎻忚堪',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.depict || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getLocAreaMatStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
+
diff --git a/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-bind-loc-dialog.vue b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-bind-loc-dialog.vue
new file mode 100644
index 0000000..49f1e76
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-bind-loc-dialog.vue
@@ -0,0 +1,199 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="960px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocAreaMatRelationBindLocModel,
+ createLocAreaMatRelationBindLocState
+ } from '../locAreaMatPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ bindData: { type: Object, default: () => ({}) },
+ warehouseOptions: { type: Array, default: () => [] },
+ areaOptions: { type: Array, default: () => [] },
+ groupOptions: { type: Array, default: () => [] },
+ locOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+
+ const formRef = ref()
+ const form = reactive(createLocAreaMatRelationBindLocState())
+
+ const dialogTitle = computed(() => '搴撲綅缁戝畾搴撳尯')
+ const selectedWarehouseId = computed(() =>
+ form.warehouseId !== undefined && form.warehouseId !== null && form.warehouseId !== ''
+ ? Number(form.warehouseId)
+ : void 0
+ )
+ const selectedAreaId = computed(() =>
+ form.areaId !== undefined && form.areaId !== null && form.areaId !== ''
+ ? Number(form.areaId)
+ : void 0
+ )
+
+ const filteredAreaOptions = computed(() => {
+ if (selectedWarehouseId.value === void 0) {
+ return props.areaOptions
+ }
+ return props.areaOptions.filter(
+ (item) => item.warehouseId === void 0 || Number(item.warehouseId) === selectedWarehouseId.value
+ )
+ })
+
+ const filteredLocOptions = computed(() => {
+ return props.locOptions.filter((item) => {
+ const warehouseMatch =
+ selectedWarehouseId.value === void 0 ||
+ item.warehouseId === void 0 ||
+ Number(item.warehouseId) === selectedWarehouseId.value
+ const areaMatch =
+ selectedAreaId.value === void 0 || item.areaId === void 0 || Number(item.areaId) === selectedAreaId.value
+ return warehouseMatch && areaMatch
+ })
+ })
+
+ const rules = computed(() => ({
+ warehouseId: [{ required: true, message: '璇烽�夋嫨浠撳簱', trigger: 'change' }],
+ areaId: [{ required: true, message: '璇烽�夋嫨搴撳尯', trigger: 'change' }],
+ groupId: [{ required: true, message: '璇烽�夋嫨鐗╂枡鍒嗙粍', trigger: 'change' }],
+ locId: [{ required: true, message: '璇烽�夋嫨搴撲綅', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '浠撳簱',
+ key: 'warehouseId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨浠撳簱',
+ clearable: true,
+ filterable: true,
+ options: props.warehouseOptions
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撳尯',
+ clearable: true,
+ filterable: true,
+ options: filteredAreaOptions.value
+ }
+ },
+ {
+ label: '鐗╂枡鍒嗙粍',
+ key: 'groupId',
+ type: 'treeselect',
+ props: {
+ data: props.groupOptions,
+ props: {
+ label: 'displayLabel',
+ value: 'id',
+ children: 'children'
+ },
+ placeholder: '璇烽�夋嫨鐗╂枡鍒嗙粍',
+ clearable: false,
+ checkStrictly: true,
+ defaultExpandAll: true
+ }
+ },
+ {
+ label: '搴撲綅',
+ key: 'locId',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨搴撲綅',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: filteredLocOptions.value
+ }
+ }
+ ])
+
+ const resetForm = () => {
+ Object.assign(form, createLocAreaMatRelationBindLocState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const loadFormData = () => {
+ Object.assign(form, buildLocAreaMatRelationBindLocModel(props.bindData))
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.bindData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-bind-matnr-dialog.vue b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-bind-matnr-dialog.vue
new file mode 100644
index 0000000..5310469
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-bind-matnr-dialog.vue
@@ -0,0 +1,212 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="960px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocAreaMatRelationBindMatnrModel,
+ createLocAreaMatRelationBindMatnrState
+ } from '../locAreaMatPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ bindData: { type: Object, default: () => ({}) },
+ warehouseOptions: { type: Array, default: () => [] },
+ areaOptions: { type: Array, default: () => [] },
+ groupOptions: { type: Array, default: () => [] },
+ matnrOptions: { type: Array, default: () => [] },
+ locTypeOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+
+ const formRef = ref()
+ const form = reactive(createLocAreaMatRelationBindMatnrState())
+
+ const dialogTitle = computed(() => '鐗╂枡缁戝畾搴撳尯')
+ const selectedWarehouseId = computed(() =>
+ form.warehouseId !== undefined && form.warehouseId !== null && form.warehouseId !== ''
+ ? Number(form.warehouseId)
+ : void 0
+ )
+ const selectedGroupId = computed(() =>
+ form.groupId !== undefined && form.groupId !== null && form.groupId !== ''
+ ? Number(form.groupId)
+ : void 0
+ )
+
+ const filteredAreaOptions = computed(() => {
+ if (selectedWarehouseId.value === void 0) {
+ return props.areaOptions
+ }
+ return props.areaOptions.filter(
+ (item) => item.warehouseId === void 0 || Number(item.warehouseId) === selectedWarehouseId.value
+ )
+ })
+
+ const filteredMatnrOptions = computed(() => {
+ if (selectedGroupId.value === void 0) {
+ return props.matnrOptions
+ }
+ return props.matnrOptions.filter(
+ (item) => item.groupId === void 0 || Number(item.groupId) === selectedGroupId.value
+ )
+ })
+
+ const rules = computed(() => ({
+ warehouseId: [{ required: true, message: '璇烽�夋嫨浠撳簱', trigger: 'change' }],
+ areaId: [{ required: true, message: '璇烽�夋嫨搴撳尯', trigger: 'change' }],
+ groupId: [{ required: true, message: '璇烽�夋嫨鐗╂枡鍒嗙粍', trigger: 'change' }],
+ matnrId: [{ required: true, message: '璇烽�夋嫨鐗╂枡', trigger: 'change' }],
+ typeId: [{ required: true, message: '璇烽�夋嫨搴撲綅绫诲瀷', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '浠撳簱',
+ key: 'warehouseId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨浠撳簱',
+ clearable: true,
+ filterable: true,
+ options: props.warehouseOptions
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撳尯',
+ clearable: true,
+ filterable: true,
+ options: filteredAreaOptions.value
+ }
+ },
+ {
+ label: '鐗╂枡鍒嗙粍',
+ key: 'groupId',
+ type: 'treeselect',
+ props: {
+ data: props.groupOptions,
+ props: {
+ label: 'displayLabel',
+ value: 'id',
+ children: 'children'
+ },
+ placeholder: '璇烽�夋嫨鐗╂枡鍒嗙粍',
+ clearable: false,
+ checkStrictly: true,
+ defaultExpandAll: true
+ }
+ },
+ {
+ label: '鐗╂枡',
+ key: 'matnrId',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨鐗╂枡',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: filteredMatnrOptions.value
+ }
+ },
+ {
+ label: '搴撲綅绫诲瀷',
+ key: 'typeId',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨搴撲綅绫诲瀷',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.locTypeOptions
+ }
+ }
+ ])
+
+ const resetForm = () => {
+ Object.assign(form, createLocAreaMatRelationBindMatnrState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const loadFormData = () => {
+ Object.assign(form, buildLocAreaMatRelationBindMatnrModel(props.bindData))
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.bindData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-detail-drawer.vue b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-detail-drawer.vue
new file mode 100644
index 0000000..045641d
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-detail-drawer.vue
@@ -0,0 +1,60 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳尯鐗╂枡璇︽儏"
+ size="1120px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="閫昏緫缂栧彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱">{{ detail.warehouseName || detail.warehouseIdText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯">{{ detail.areaName || detail.areaIdText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閫昏緫鎻忚堪">{{ detail.depict || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <LocAreaMatRelationPanel :area-mat-data="detail" />
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import LocAreaMatRelationPanel from './loc-area-mat-relation-panel.vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-dialog.vue b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-dialog.vue
new file mode 100644
index 0000000..3ee1b5e
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-dialog.vue
@@ -0,0 +1,198 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="920px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocAreaMatDialogModel,
+ createLocAreaMatFormState
+ } from '../locAreaMatPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ locAreaMatData: { type: Object, default: () => ({}) },
+ warehouseOptions: { type: Array, default: () => [] },
+ areaOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+
+ const formRef = ref()
+ const form = reactive(createLocAreaMatFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫搴撳尯鐗╂枡' : '鏂板搴撳尯鐗╂枡'))
+
+ const selectedWarehouseId = computed(() =>
+ form.warehouseId !== void 0 && form.warehouseId !== null && form.warehouseId !== ''
+ ? Number(form.warehouseId)
+ : void 0
+ )
+
+ const filteredAreaOptions = computed(() => {
+ if (selectedWarehouseId.value === void 0) {
+ return props.areaOptions
+ }
+ return props.areaOptions.filter(
+ (item) => item.warehouseId === void 0 || Number(item.warehouseId) === selectedWarehouseId.value
+ )
+ })
+
+ const rules = computed(() => ({
+ code: [{ required: true, message: '璇疯緭鍏ラ�昏緫缂栧彿', trigger: 'blur' }],
+ warehouseId: [{ required: true, message: '璇烽�夋嫨浠撳簱', trigger: 'change' }],
+ areaId: [{ required: true, message: '璇烽�夋嫨搴撳尯', trigger: 'change' }],
+ depict: [{ required: true, message: '璇疯緭鍏ラ�昏緫鎻忚堪', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '閫昏緫缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ�昏緫缂栧彿',
+ clearable: true
+ }
+ },
+ {
+ label: '浠撳簱',
+ key: 'warehouseId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨浠撳簱',
+ clearable: true,
+ filterable: true,
+ options: props.warehouseOptions
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撳尯',
+ clearable: true,
+ filterable: true,
+ options: filteredAreaOptions.value
+ }
+ },
+ {
+ label: '閫昏緫鎻忚堪',
+ key: 'depict',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ラ�昏緫鎻忚堪',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const resetForm = () => {
+ Object.assign(form, createLocAreaMatFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const loadFormData = () => {
+ Object.assign(form, buildLocAreaMatDialogModel(props.locAreaMatData))
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.locAreaMatData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-relation-panel.vue b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-relation-panel.vue
new file mode 100644
index 0000000..8a250b8
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-mat/modules/loc-area-mat-relation-panel.vue
@@ -0,0 +1,435 @@
+<template>
+ <ElCard class="art-table-card" shadow="never">
+ <template #header>
+ <div class="flex items-center justify-between gap-4">
+ <div>
+ <div class="text-base font-medium text-[var(--art-text-primary)]">鍏崇郴绠$悊</div>
+ <div class="text-xs text-[var(--art-text-secondary)]">
+ {{ panelSubtitle }}
+ </div>
+ </div>
+
+ <ElSpace wrap>
+ <ElButton size="small" @click="handleRefresh" v-ripple>鍒锋柊</ElButton>
+ <ElButton size="small" type="primary" @click="openBindMatnrDialog" v-ripple>缁戝畾鐗╂枡</ElButton>
+ <ElButton size="small" type="success" @click="openBindLocDialog" v-ripple>缁戝畾搴撲綅</ElButton>
+ <ElButton
+ size="small"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </div>
+ </template>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <LocAreaMatBindMatnrDialog
+ v-model:visible="bindMatnrDialogVisible"
+ :bind-data="bindMatnrData"
+ :warehouse-options="warehouseOptions"
+ :area-options="areaOptions"
+ :group-options="groupOptions"
+ :matnr-options="matnrOptions"
+ :loc-type-options="locTypeOptions"
+ @submit="handleBindMatnrSubmit"
+ />
+
+ <LocAreaMatBindLocDialog
+ v-model:visible="bindLocDialogVisible"
+ :bind-data="bindLocData"
+ :warehouse-options="warehouseOptions"
+ :area-options="areaOptions"
+ :group-options="groupOptions"
+ :loc-options="locOptions"
+ @submit="handleBindLocSubmit"
+ />
+ </ElCard>
+</template>
+
+<script setup>
+ import { computed, h, onMounted, ref, watch } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchWarehouseAreasList, fetchWarehouseList, fetchLocPage, fetchLocTypeList } from '@/api/loc'
+ import { fetchMatnrGroupTree, fetchMatnrPage } from '@/api/wh-mat'
+ import {
+ fetchBindLocAreaMatByMatnr,
+ fetchDeleteLocAreaMatRela,
+ fetchLocAreaMatRelaPage
+ } from '@/api/loc-area-mat'
+ import {
+ buildLocAreaMatRelationBindMatnrModel,
+ buildLocAreaMatRelationBindLocModel,
+ normalizeLocAreaMatRelationRow,
+ normalizeLocAreaMatGroupTreeOptions,
+ resolveLocAreaMatAreaOptions,
+ resolveLocAreaMatLocOptions,
+ resolveLocAreaMatLocTypeOptions,
+ resolveLocAreaMatMatnrOptions,
+ resolveLocAreaMatWarehouseOptions
+ } from '../locAreaMatPage.helpers'
+ import { createLocAreaMatRelationBindMatnrState } from '../locAreaMatPage.helpers'
+ import LocAreaMatBindMatnrDialog from './loc-area-mat-bind-matnr-dialog.vue'
+ import LocAreaMatBindLocDialog from './loc-area-mat-bind-loc-dialog.vue'
+ import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+
+ const props = defineProps({
+ areaMatData: { type: Object, default: () => ({}) }
+ })
+
+ const warehouseOptions = ref([])
+ const areaOptions = ref([])
+ const groupOptions = ref([])
+ const matnrOptions = ref([])
+ const locTypeOptions = ref([])
+ const locOptions = ref([])
+ const bindMatnrDialogVisible = ref(false)
+ const bindLocDialogVisible = ref(false)
+
+ const areaMatId = computed(() =>
+ props.areaMatData?.id !== void 0 && props.areaMatData?.id !== null && props.areaMatData?.id !== ''
+ ? Number(props.areaMatData.id)
+ : void 0
+ )
+
+ const bindMatnrData = computed(() =>
+ buildLocAreaMatRelationBindMatnrModel({
+ ...createLocAreaMatRelationBindMatnrState(areaMatId.value),
+ areaMatId: areaMatId.value,
+ warehouseId: props.areaMatData?.warehouseId,
+ areaId: props.areaMatData?.areaId
+ })
+ )
+
+ const bindLocData = computed(() =>
+ buildLocAreaMatRelationBindLocModel({
+ areaMatId: areaMatId.value,
+ warehouseId: props.areaMatData?.warehouseId,
+ areaId: props.areaMatData?.areaId
+ })
+ )
+
+ const panelSubtitle = computed(() => {
+ if (!areaMatId.value) {
+ return '璇烽�夋嫨搴撳尯鐗╂枡鏌ョ湅鍏崇郴鏄庣粏'
+ }
+ const code = String(props.areaMatData?.code || '').trim()
+ const depict = String(props.areaMatData?.depict || '').trim()
+ return [code, depict].filter(Boolean).join(' 路 ') || `涓诲崟ID ${areaMatId.value}`
+ })
+
+ const columns = computed(() => [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'relationTypeText',
+ label: '缁戝畾绫诲瀷',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.relationTypeText || '--'
+ },
+ {
+ prop: 'warehouseIdText',
+ label: '浠撳簱',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.warehouseIdText || '--'
+ },
+ {
+ prop: 'areaIdText',
+ label: '搴撳尯',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.areaIdText || '--'
+ },
+ {
+ prop: 'groupIdText',
+ label: '鐗╂枡鍒嗙粍',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.groupIdText || '--'
+ },
+ {
+ prop: 'matnrIdText',
+ label: '鐗╂枡',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrIdText || '--'
+ },
+ {
+ prop: 'locTypeIdText',
+ label: '搴撲綅绫诲瀷',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locTypeIdText || '--'
+ },
+ {
+ prop: 'locIdText',
+ label: '搴撲綅',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locIdText || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 120,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: [{ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' }],
+ onClick: (item) => {
+ if (item.key === 'delete') {
+ handleDeleteRelation(row)
+ }
+ }
+ })
+ }
+ ])
+
+ const {
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchLocAreaMatRelaPage,
+ apiParams: {
+ current: 1,
+ pageSize: 20
+ },
+ immediate: false,
+ columnsFactory: () => columns.value,
+ paginationKey: {
+ current: 'current',
+ size: 'pageSize'
+ }
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeLocAreaMatRelationRow(item))
+ }
+ }
+ })
+
+ const selectedRows = ref([])
+
+ const handleSelectionChange = (rows) => {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ const handleRefresh = async () => {
+ await refreshData()
+ }
+
+ async function loadRelationData() {
+ if (!areaMatId.value) {
+ selectedRows.value = []
+ replaceSearchParams({ areaMatId: void 0 })
+ return
+ }
+ replaceSearchParams({
+ areaMatId: areaMatId.value
+ })
+ await getData({
+ areaMatId: areaMatId.value
+ })
+ }
+
+ async function handleDeleteRelation(record) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄ゅ叧绯汇��${record?.relationTypeText || record?.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteLocAreaMatRela(record.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshData()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ async function handleBatchDelete() {
+ if (!selectedRows.value.length) {
+ return
+ }
+ const ids = selectedRows.value
+ .map((item) => item.id)
+ .filter((id) => id !== void 0 && id !== null)
+ if (!ids.length) {
+ return
+ }
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佹壒閲忓垹闄ら�変腑鐨� ${ids.length} 鏉″叧绯诲悧锛焋, '鎵归噺鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteLocAreaMatRela(ids.join(','))
+ ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+ selectedRows.value = []
+ await refreshData()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鎵归噺鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ function openBindMatnrDialog() {
+ if (!areaMatId.value) {
+ ElMessage.warning('璇峰厛閫夋嫨搴撳尯鐗╂枡涓诲崟')
+ return
+ }
+ bindMatnrDialogVisible.value = true
+ }
+
+ function openBindLocDialog() {
+ if (!areaMatId.value) {
+ ElMessage.warning('璇峰厛閫夋嫨搴撳尯鐗╂枡涓诲崟')
+ return
+ }
+ bindLocDialogVisible.value = true
+ }
+
+ async function handleBindMatnrSubmit(formData) {
+ try {
+ await fetchBindLocAreaMatByMatnr({
+ areaMatId: areaMatId.value,
+ warehouseId: formData.warehouseId,
+ areaId: formData.areaId,
+ groupId: formData.groupId,
+ matnrId: formData.matnrId,
+ typeId: formData.typeId
+ })
+ ElMessage.success('鐗╂枡缁戝畾鎴愬姛')
+ bindMatnrDialogVisible.value = false
+ await refreshData()
+ } catch (error) {
+ ElMessage.error(error?.message || '鐗╂枡缁戝畾澶辫触')
+ }
+ }
+
+ async function handleBindLocSubmit(formData) {
+ try {
+ await fetchBindLocAreaMatByMatnr({
+ areaMatId: areaMatId.value,
+ warehouseId: formData.warehouseId,
+ areaId: formData.areaId,
+ groupId: formData.groupId,
+ locId: formData.locId
+ })
+ ElMessage.success('搴撲綅缁戝畾鎴愬姛')
+ bindLocDialogVisible.value = false
+ await refreshData()
+ } catch (error) {
+ ElMessage.error(error?.message || '搴撲綅缁戝畾澶辫触')
+ }
+ }
+
+ async function loadWarehouseOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseList(), [], {
+ timeoutMessage: '浠撳簱閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ warehouseOptions.value = resolveLocAreaMatWarehouseOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadAreaOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveLocAreaMatAreaOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadGroupOptions() {
+ const records = await guardRequestWithMessage(fetchMatnrGroupTree({}), [], {
+ timeoutMessage: '鐗╂枡鍒嗙粍閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ groupOptions.value = normalizeLocAreaMatGroupTreeOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadMatnrOptions() {
+ const response = await guardRequestWithMessage(
+ fetchMatnrPage({ current: 1, pageSize: 200 }),
+ { records: [] },
+ { timeoutMessage: '鐗╂枡閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ matnrOptions.value = resolveLocAreaMatMatnrOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadLocTypeOptions() {
+ const records = await guardRequestWithMessage(fetchLocTypeList(), [], {
+ timeoutMessage: '搴撲綅绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ locTypeOptions.value = resolveLocAreaMatLocTypeOptions(defaultResponseAdapter(records).records)
+ }
+
+ async function loadLocOptions() {
+ const response = await guardRequestWithMessage(
+ fetchLocPage({ current: 1, pageSize: 200 }),
+ { records: [] },
+ { timeoutMessage: '搴撲綅閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ locOptions.value = resolveLocAreaMatLocOptions(defaultResponseAdapter(response).records)
+ }
+
+ watch(
+ () => props.areaMatData,
+ () => {
+ loadRelationData()
+ },
+ { deep: true, immediate: true }
+ )
+
+ onMounted(async () => {
+ await Promise.all([
+ loadWarehouseOptions(),
+ loadAreaOptions(),
+ loadGroupOptions(),
+ loadMatnrOptions(),
+ loadLocTypeOptions(),
+ loadLocOptions()
+ ])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-rela/index.vue b/rsf-design/src/views/basic-info/loc-area-rela/index.vue
new file mode 100644
index 0000000..34c5be8
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-rela/index.vue
@@ -0,0 +1,361 @@
+<template>
+ <div class="loc-area-rela-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板搴撳尯鍏崇郴</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <LocAreaRelaDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :loc-area-rela-data="currentLocAreaRelaData"
+ @submit="handleDialogSubmit"
+ />
+
+ <LocAreaRelaDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchDeleteLocAreaRela,
+ fetchExportLocAreaRelaReport,
+ fetchGetLocAreaRelaDetail,
+ fetchLocAreaRelaMany,
+ fetchLocAreaRelaPage,
+ fetchSaveLocAreaRela,
+ fetchUpdateLocAreaRela
+ } from '@/api/loc-area-rela'
+ import LocAreaRelaDialog from './modules/loc-area-rela-dialog.vue'
+ import LocAreaRelaDetailDrawer from './modules/loc-area-rela-detail-drawer.vue'
+ import { createLocAreaRelaTableColumns } from './locAreaRelaTable.columns'
+ import {
+ LOC_AREA_RELA_REPORT_STYLE,
+ LOC_AREA_RELA_REPORT_TITLE,
+ buildLocAreaRelaDialogModel,
+ buildLocAreaRelaPageQueryParams,
+ buildLocAreaRelaPrintRows,
+ buildLocAreaRelaReportMeta,
+ buildLocAreaRelaSavePayload,
+ buildLocAreaRelaSearchParams,
+ createLocAreaRelaSearchState,
+ getLocAreaRelaPaginationKey,
+ getLocAreaRelaStatusOptions,
+ normalizeLocAreaRelaListRow
+ } from './locAreaRelaPage.helpers'
+
+ defineOptions({ name: 'LocAreaRela' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createLocAreaRelaSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = LOC_AREA_RELA_REPORT_TITLE
+ const reportQueryParams = computed(() => buildLocAreaRelaSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ垎鍖篒D/搴撲綅ID/澶囨敞'
+ }
+ },
+ {
+ label: '鍒嗗尯ID',
+ key: 'locAreaId',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ垎鍖篒D'
+ }
+ },
+ {
+ label: '搴撲綅ID',
+ key: 'locId',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ簱浣岻D'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getLocAreaRelaStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨寮�濮嬫棩鏈�'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨缁撴潫鏃ユ湡'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(
+ fetchGetLocAreaRelaDetail(row.id),
+ {},
+ {
+ timeoutMessage: '搴撳尯鍏崇郴璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ detailData.value = normalizeLocAreaRelaListRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撳尯鍏崇郴璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(
+ fetchGetLocAreaRelaDetail(row.id),
+ {},
+ {
+ timeoutMessage: '搴撳尯鍏崇郴璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇搴撳尯鍏崇郴璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchLocAreaRelaPage,
+ apiParams: buildLocAreaRelaPageQueryParams(searchForm.value),
+ paginationKey: getLocAreaRelaPaginationKey(),
+ columnsFactory: () =>
+ createLocAreaRelaTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeLocAreaRelaListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentLocAreaRelaData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildLocAreaRelaDialogModel(),
+ buildEditModel: (record) => buildLocAreaRelaDialogModel(record),
+ buildSavePayload: (formData) => buildLocAreaRelaSavePayload(formData),
+ saveRequest: fetchSaveLocAreaRela,
+ updateRequest: fetchUpdateLocAreaRela,
+ deleteRequest: fetchDeleteLocAreaRela,
+ entityName: '搴撳尯鍏崇郴',
+ resolveRecordLabel: (record) => record?.locAreaId || record?.locId || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...LOC_AREA_RELA_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchLocAreaRelaMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchLocAreaRelaPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'loc-area-rela.xlsx',
+ requestExport: (payload) =>
+ fetchExportLocAreaRelaReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildLocAreaRelaPrintRows(records),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildLocAreaRelaReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || LOC_AREA_RELA_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildLocAreaRelaSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createLocAreaRelaSearchState())
+ resetSearchParams()
+ }
+
+ onMounted(async () => {
+ await getData()
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-rela/locAreaRelaPage.helpers.js b/rsf-design/src/views/basic-info/loc-area-rela/locAreaRelaPage.helpers.js
new file mode 100644
index 0000000..e55dfc9
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-rela/locAreaRelaPage.helpers.js
@@ -0,0 +1,193 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const LOC_AREA_RELA_REPORT_TITLE = '搴撳尯鍏崇郴鎶ヨ〃'
+export const LOC_AREA_RELA_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+export function createLocAreaRelaSearchState() {
+ return {
+ condition: '',
+ locAreaId: '',
+ locId: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createLocAreaRelaFormState() {
+ return {
+ id: void 0,
+ locAreaId: void 0,
+ locId: void 0,
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getLocAreaRelaPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getLocAreaRelaStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getLocAreaRelaStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildLocAreaRelaSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ locAreaId:
+ params.locAreaId !== undefined && params.locAreaId !== null && params.locAreaId !== ''
+ ? normalizeNumber(params.locAreaId)
+ : void 0,
+ locId:
+ params.locId !== undefined && params.locId !== null && params.locId !== ''
+ ? normalizeNumber(params.locId)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(
+ ([, value]) => value !== '' && value !== void 0 && value !== null
+ )
+ )
+}
+
+export function buildLocAreaRelaPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildLocAreaRelaSearchParams(params)
+ }
+}
+
+export function buildLocAreaRelaSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: normalizeNumber(formData.id) }
+ : {}),
+ ...(formData.locAreaId !== undefined && formData.locAreaId !== null && formData.locAreaId !== ''
+ ? { locAreaId: normalizeNumber(formData.locAreaId) }
+ : {}),
+ ...(formData.locId !== undefined && formData.locId !== null && formData.locId !== ''
+ ? { locId: normalizeNumber(formData.locId) }
+ : {}),
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? normalizeNumber(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildLocAreaRelaDialogModel(record = {}) {
+ return {
+ ...createLocAreaRelaFormState(),
+ ...(record.id !== undefined && record.id !== null && record.id !== ''
+ ? { id: normalizeNumber(record.id) }
+ : {}),
+ locAreaId:
+ record.locAreaId !== undefined && record.locAreaId !== null && record.locAreaId !== ''
+ ? normalizeNumber(record.locAreaId)
+ : void 0,
+ locId:
+ record.locId !== undefined && record.locId !== null && record.locId !== ''
+ ? normalizeNumber(record.locId)
+ : void 0,
+ status:
+ record.status !== undefined && record.status !== null ? normalizeNumber(record.status, 1) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeLocAreaRelaDetailRecord(record = {}) {
+ const statusMeta = getLocAreaRelaStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ locAreaIdText: normalizeText(
+ record.locAreaId$ || record.locAreaIdText || record.locAreaId || ''
+ ),
+ locIdText: normalizeText(record.locId$ || record.locIdText || record.locId || ''),
+ memo: normalizeText(record.memo || ''),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeLocAreaRelaListRow(record = {}) {
+ return normalizeLocAreaRelaDetailRecord(record)
+}
+
+export function buildLocAreaRelaPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeLocAreaRelaListRow(record))
+}
+
+export function buildLocAreaRelaReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = LOC_AREA_RELA_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: LOC_AREA_RELA_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...LOC_AREA_RELA_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/loc-area-rela/locAreaRelaTable.columns.js b/rsf-design/src/views/basic-info/loc-area-rela/locAreaRelaTable.columns.js
new file mode 100644
index 0000000..bdfd92d
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-rela/locAreaRelaTable.columns.js
@@ -0,0 +1,115 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getLocAreaRelaStatusMeta } from './locAreaRelaPage.helpers'
+
+export function createLocAreaRelaTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({
+ key: 'delete',
+ label: '鍒犻櫎',
+ icon: 'ri:delete-bin-5-line',
+ color: 'var(--art-error)'
+ })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'locAreaIdText',
+ label: '鍒嗗尯ID',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locAreaIdText || row.locAreaId || '--'
+ },
+ {
+ prop: 'locIdText',
+ label: '搴撲綅ID',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locIdText || row.locId || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getLocAreaRelaStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/loc-area-rela/modules/loc-area-rela-detail-drawer.vue b/rsf-design/src/views/basic-info/loc-area-rela/modules/loc-area-rela-detail-drawer.vue
new file mode 100644
index 0000000..088f2e6
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-rela/modules/loc-area-rela-detail-drawer.vue
@@ -0,0 +1,63 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳尯鍏崇郴璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="10" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒嗗尯ID">{{
+ detail.locAreaIdText || detail.locAreaId || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅ID">{{
+ detail.locIdText || detail.locId || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{
+ detail.createTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{
+ detail.updateTimeText || '--'
+ }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area-rela/modules/loc-area-rela-dialog.vue b/rsf-design/src/views/basic-info/loc-area-rela/modules/loc-area-rela-dialog.vue
new file mode 100644
index 0000000..766188f
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area-rela/modules/loc-area-rela-dialog.vue
@@ -0,0 +1,152 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="720px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocAreaRelaDialogModel,
+ createLocAreaRelaFormState,
+ getLocAreaRelaStatusOptions
+ } from '../locAreaRelaPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ locAreaRelaData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createLocAreaRelaFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫搴撳尯鍏崇郴' : '鏂板搴撳尯鍏崇郴'))
+
+ const rules = computed(() => ({
+ locAreaId: [{ required: true, message: '璇疯緭鍏ュ垎鍖篒D', trigger: 'blur' }],
+ locId: [{ required: true, message: '璇疯緭鍏ュ簱浣岻D', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '鍒嗗尯ID',
+ key: 'locAreaId',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ垎鍖篒D'
+ }
+ },
+ {
+ label: '搴撲綅ID',
+ key: 'locId',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ簱浣岻D'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: getLocAreaRelaStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildLocAreaRelaDialogModel(props.locAreaRelaData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createLocAreaRelaFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.locAreaRelaData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area/index.vue b/rsf-design/src/views/basic-info/loc-area/index.vue
new file mode 100644
index 0000000..8d3032c
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area/index.vue
@@ -0,0 +1,345 @@
+<template>
+ <div class="loc-area-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板搴撳尯</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <LocAreaDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :loc-area-data="currentLocAreaData"
+ :area-options="areaOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <LocAreaDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
+ import {
+ fetchDeleteLocArea,
+ fetchExportLocAreaReport,
+ fetchGetLocAreaDetail,
+ fetchLocAreaMany,
+ fetchLocAreaPage,
+ fetchSaveLocArea,
+ fetchUpdateLocArea
+ } from '@/api/loc-area'
+ import LocAreaDialog from './modules/loc-area-dialog.vue'
+ import LocAreaDetailDrawer from './modules/loc-area-detail-drawer.vue'
+ import { createLocAreaTableColumns } from './locAreaTable.columns'
+ import {
+ LOC_AREA_REPORT_STYLE,
+ LOC_AREA_REPORT_TITLE,
+ buildLocAreaDialogModel,
+ buildLocAreaPageQueryParams,
+ buildLocAreaPrintRows,
+ buildLocAreaReportMeta,
+ buildLocAreaSavePayload,
+ buildLocAreaSearchParams,
+ createLocAreaSearchState,
+ getLocAreaPaginationKey,
+ getLocAreaStatusOptions,
+ normalizeLocAreaListRow,
+ resolveLocAreaOptions
+ } from './locAreaPage.helpers'
+
+ defineOptions({ name: 'LocArea' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createLocAreaSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const areaOptions = ref([])
+ let handleDeleteAction = null
+
+ const reportTitle = LOC_AREA_REPORT_TITLE
+ const reportQueryParams = computed(() => buildLocAreaSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱鍖哄悕绉�/缂栫爜/澶囨敞'
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: areaOptions.value
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悕绉�'
+ }
+ },
+ {
+ label: '缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鐮�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getLocAreaStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocAreaDetail(row.id), {}, {
+ timeoutMessage: '搴撳尯璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeLocAreaListRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撳尯璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocAreaDetail(row.id), {}, {
+ timeoutMessage: '搴撳尯璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇搴撳尯璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchLocAreaPage,
+ apiParams: buildLocAreaPageQueryParams(searchForm.value),
+ paginationKey: getLocAreaPaginationKey(),
+ columnsFactory: () =>
+ createLocAreaTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeLocAreaListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentLocAreaData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildLocAreaDialogModel(),
+ buildEditModel: (record) => buildLocAreaDialogModel(record),
+ buildSavePayload: (formData) => buildLocAreaSavePayload(formData),
+ saveRequest: fetchSaveLocArea,
+ updateRequest: fetchUpdateLocArea,
+ deleteRequest: fetchDeleteLocArea,
+ entityName: '搴撳尯',
+ resolveRecordLabel: (record) => record?.areaName || record?.name || record?.code || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ async function loadAreaOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveLocAreaOptions(defaultResponseAdapter(records).records)
+ }
+
+ const buildPreviewDialogMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ const response = Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchLocAreaMany(payload.ids)
+ : await fetchLocAreaPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'loc-area.xlsx',
+ requestExport: (payload) =>
+ fetchExportLocAreaReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildLocAreaPrintRows(records),
+ buildPreviewMeta: buildPreviewDialogMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildLocAreaReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || LOC_AREA_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildLocAreaSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createLocAreaSearchState())
+ resetSearchParams()
+ }
+
+ onMounted(async () => {
+ await Promise.all([loadAreaOptions(), getData()])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area/locAreaPage.helpers.js b/rsf-design/src/views/basic-info/loc-area/locAreaPage.helpers.js
new file mode 100644
index 0000000..2441e70
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area/locAreaPage.helpers.js
@@ -0,0 +1,202 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const LOC_AREA_REPORT_TITLE = '搴撳尯鎶ヨ〃'
+export const LOC_AREA_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+export function createLocAreaSearchState() {
+ return {
+ condition: '',
+ areaId: '',
+ name: '',
+ code: '',
+ status: '',
+ memo: ''
+ }
+}
+
+export function createLocAreaFormState() {
+ return {
+ id: void 0,
+ areaId: void 0,
+ name: '',
+ code: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getLocAreaPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getLocAreaStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getLocAreaStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildLocAreaSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ areaId:
+ params.areaId !== undefined && params.areaId !== null && params.areaId !== ''
+ ? Number(params.areaId)
+ : void 0,
+ name: normalizeText(params.name),
+ code: normalizeText(params.code),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocAreaPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildLocAreaSearchParams(params)
+ }
+}
+
+export function buildLocAreaSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.areaId !== undefined && formData.areaId !== null && formData.areaId !== ''
+ ? { areaId: Number(formData.areaId) }
+ : {}),
+ name: normalizeText(formData.name) || '',
+ code: normalizeText(formData.code) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildLocAreaDialogModel(record = {}) {
+ return {
+ ...createLocAreaFormState(),
+ ...(record.id !== undefined && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ areaId:
+ record.areaId !== undefined && record.areaId !== null && record.areaId !== ''
+ ? Number(record.areaId)
+ : void 0,
+ name: normalizeText(record.name || ''),
+ code: normalizeText(record.code || ''),
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function resolveLocAreaOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.name || item.code || `搴撳尯 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function normalizeLocAreaDetailRecord(record = {}) {
+ const statusMeta = getLocAreaStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ areaName: normalizeText(record.areaId$ || record.areaName || ''),
+ name: normalizeText(record.name || ''),
+ code: normalizeText(record.code || ''),
+ memo: normalizeText(record.memo || ''),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeLocAreaListRow(record = {}) {
+ return normalizeLocAreaDetailRecord(record)
+}
+
+export function buildLocAreaPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeLocAreaListRow(record))
+}
+
+export function buildLocAreaReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = LOC_AREA_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: LOC_AREA_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...LOC_AREA_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/loc-area/locAreaTable.columns.js b/rsf-design/src/views/basic-info/loc-area/locAreaTable.columns.js
new file mode 100644
index 0000000..d54c857
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area/locAreaTable.columns.js
@@ -0,0 +1,108 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getLocAreaStatusMeta } from './locAreaPage.helpers'
+
+export function createLocAreaTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'areaName',
+ label: '搴撳尯',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.areaName || row.areaId$ || row.areaId || '--'
+ },
+ {
+ prop: 'name',
+ label: '鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.name || '--'
+ },
+ {
+ prop: 'code',
+ label: '缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getLocAreaStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/loc-area/modules/loc-area-detail-drawer.vue b/rsf-design/src/views/basic-info/loc-area/modules/loc-area-detail-drawer.vue
new file mode 100644
index 0000000..5bfc323
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area/modules/loc-area-detail-drawer.vue
@@ -0,0 +1,56 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳尯璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="10" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="搴撳尯">{{ detail.areaName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚嶇О">{{ detail.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缂栫爜">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-area/modules/loc-area-dialog.vue b/rsf-design/src/views/basic-info/loc-area/modules/loc-area-dialog.vue
new file mode 100644
index 0000000..94c9009
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-area/modules/loc-area-dialog.vue
@@ -0,0 +1,162 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="720px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocAreaDialogModel,
+ createLocAreaFormState,
+ getLocAreaStatusOptions
+ } from '../locAreaPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ locAreaData: { type: Object, default: () => ({}) },
+ areaOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createLocAreaFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫搴撳尯' : '鏂板搴撳尯'))
+
+ const rules = computed(() => ({
+ areaId: [{ required: true, message: '璇烽�夋嫨搴撳尯', trigger: 'change' }],
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨搴撳尯',
+ clearable: true,
+ filterable: true,
+ options: props.areaOptions
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: getLocAreaStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildLocAreaDialogModel(props.locAreaData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createLocAreaFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.locAreaData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-type/index.vue b/rsf-design/src/views/basic-info/loc-type/index.vue
new file mode 100644
index 0000000..b686593
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-type/index.vue
@@ -0,0 +1,342 @@
+<template>
+ <div class="loc-type-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板搴撲綅绫诲瀷</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <LocTypeDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :loc-type-data="currentLocTypeData"
+ @submit="handleDialogSubmit"
+ />
+
+ <LocTypeDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchDeleteLocType,
+ fetchExportLocTypeReport,
+ fetchGetLocTypeDetail,
+ fetchLocTypeMany,
+ fetchLocTypePage,
+ fetchSaveLocType,
+ fetchUpdateLocType
+ } from '@/api/loc-type'
+ import LocTypeDialog from './modules/loc-type-dialog.vue'
+ import LocTypeDetailDrawer from './modules/loc-type-detail-drawer.vue'
+ import { createLocTypeTableColumns } from './locTypeTable.columns'
+ import {
+ LOC_TYPE_REPORT_STYLE,
+ LOC_TYPE_REPORT_TITLE,
+ buildLocTypeDialogModel,
+ buildLocTypePageQueryParams,
+ buildLocTypePrintRows,
+ buildLocTypeReportMeta,
+ buildLocTypeSavePayload,
+ buildLocTypeSearchParams,
+ createLocTypeSearchState,
+ getLocTypePaginationKey,
+ getLocTypeStatusOptions,
+ normalizeLocTypeListRow
+ } from './locTypePage.helpers'
+
+ defineOptions({ name: 'LocType' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createLocTypeSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = LOC_TYPE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildLocTypeSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鐮�/鍚嶇О/鏍囪瘑/澶囨敞'
+ }
+ },
+ {
+ label: '鏍囪瘑',
+ key: 'uuid',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ爣璇�'
+ }
+ },
+ {
+ label: '缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鐮�'
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悕绉�'
+ }
+ },
+ {
+ label: '鏉$爜瑙勫垯',
+ key: 'regex',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯鐮佽鍒�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getLocTypeStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocTypeDetail(row.id), {}, {
+ timeoutMessage: '搴撲綅绫诲瀷璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeLocTypeListRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撲綅绫诲瀷璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocTypeDetail(row.id), {}, {
+ timeoutMessage: '搴撲綅绫诲瀷璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇搴撲綅绫诲瀷璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchLocTypePage,
+ apiParams: buildLocTypePageQueryParams(searchForm.value),
+ paginationKey: getLocTypePaginationKey(),
+ columnsFactory: () =>
+ createLocTypeTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeLocTypeListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentLocTypeData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildLocTypeDialogModel(),
+ buildEditModel: (record) => buildLocTypeDialogModel(record),
+ buildSavePayload: (formData) => buildLocTypeSavePayload(formData),
+ saveRequest: fetchSaveLocType,
+ updateRequest: fetchUpdateLocType,
+ deleteRequest: fetchDeleteLocType,
+ entityName: '搴撲綅绫诲瀷',
+ resolveRecordLabel: (record) => record?.name || record?.code || record?.uuid || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewDialogMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ const response = Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchLocTypeMany(payload.ids)
+ : await fetchLocTypePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'loc-type.xlsx',
+ requestExport: (payload) =>
+ fetchExportLocTypeReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildLocTypePrintRows(records),
+ buildPreviewMeta: buildPreviewDialogMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildLocTypeReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || LOC_TYPE_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildLocTypeSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createLocTypeSearchState())
+ resetSearchParams()
+ }
+
+ onMounted(async () => {
+ await getData()
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-type/locTypePage.helpers.js b/rsf-design/src/views/basic-info/loc-type/locTypePage.helpers.js
new file mode 100644
index 0000000..8819c81
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-type/locTypePage.helpers.js
@@ -0,0 +1,175 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const LOC_TYPE_REPORT_TITLE = '搴撲綅绫诲瀷鎶ヨ〃'
+export const LOC_TYPE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+export function createLocTypeSearchState() {
+ return {
+ condition: '',
+ uuid: '',
+ code: '',
+ name: '',
+ regex: '',
+ status: '',
+ memo: ''
+ }
+}
+
+export function createLocTypeFormState() {
+ return {
+ id: void 0,
+ code: '',
+ name: '',
+ regex: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getLocTypePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getLocTypeStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getLocTypeStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildLocTypeSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ uuid: normalizeText(params.uuid),
+ code: normalizeText(params.code),
+ name: normalizeText(params.name),
+ regex: normalizeText(params.regex),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildLocTypePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildLocTypeSearchParams(params)
+ }
+}
+
+export function buildLocTypeSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ name: normalizeText(formData.name) || '',
+ regex: normalizeText(formData.regex) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildLocTypeDialogModel(record = {}) {
+ return {
+ ...createLocTypeFormState(),
+ ...(record.id !== undefined && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ code: normalizeText(record.code || ''),
+ name: normalizeText(record.name || ''),
+ regex: normalizeText(record.regex || ''),
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeLocTypeDetailRecord(record = {}) {
+ const statusMeta = getLocTypeStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ uuid: normalizeText(record.uuid || ''),
+ code: normalizeText(record.code || ''),
+ name: normalizeText(record.name || ''),
+ regex: normalizeText(record.regex || ''),
+ memo: normalizeText(record.memo || ''),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeLocTypeListRow(record = {}) {
+ return normalizeLocTypeDetailRecord(record)
+}
+
+export function buildLocTypePrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeLocTypeListRow(record))
+}
+
+export function buildLocTypeReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = LOC_TYPE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: LOC_TYPE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...LOC_TYPE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/loc-type/locTypeTable.columns.js b/rsf-design/src/views/basic-info/loc-type/locTypeTable.columns.js
new file mode 100644
index 0000000..c6ef1c8
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-type/locTypeTable.columns.js
@@ -0,0 +1,115 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getLocTypeStatusMeta } from './locTypePage.helpers'
+
+export function createLocTypeTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'uuid',
+ label: '鏍囪瘑',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.uuid || '--'
+ },
+ {
+ prop: 'code',
+ label: '缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'name',
+ label: '鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.name || '--'
+ },
+ {
+ prop: 'regex',
+ label: '鏉$爜瑙勫垯',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.regex || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getLocTypeStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/loc-type/modules/loc-type-detail-drawer.vue b/rsf-design/src/views/basic-info/loc-type/modules/loc-type-detail-drawer.vue
new file mode 100644
index 0000000..672caac
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-type/modules/loc-type-detail-drawer.vue
@@ -0,0 +1,57 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撲綅绫诲瀷璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鏍囪瘑">{{ detail.uuid || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缂栫爜">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚嶇О">{{ detail.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉$爜瑙勫垯">{{ detail.regex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/loc-type/modules/loc-type-dialog.vue b/rsf-design/src/views/basic-info/loc-type/modules/loc-type-dialog.vue
new file mode 100644
index 0000000..9c5a74d
--- /dev/null
+++ b/rsf-design/src/views/basic-info/loc-type/modules/loc-type-dialog.vue
@@ -0,0 +1,161 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="920px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocTypeDialogModel,
+ createLocTypeFormState,
+ getLocTypeStatusOptions
+ } from '../locTypePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ locTypeData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createLocTypeFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫搴撲綅绫诲瀷' : '鏂板搴撲綅绫诲瀷'))
+
+ const rules = computed(() => ({
+ code: [{ required: true, message: '璇疯緭鍏ョ紪鐮�', trigger: 'blur' }],
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }],
+ regex: [{ required: true, message: '璇疯緭鍏ユ潯鐮佽鍒�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '鏉$爜瑙勫垯',
+ key: 'regex',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ユ潯鐮佽鍒�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: getLocTypeStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildLocTypeDialogModel(props.locTypeData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createLocTypeFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.locTypeData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/matnr-group/index.vue b/rsf-design/src/views/basic-info/matnr-group/index.vue
new file mode 100644
index 0000000..dadb999
--- /dev/null
+++ b/rsf-design/src/views/basic-info/matnr-group/index.vue
@@ -0,0 +1,501 @@
+<template>
+ <div class="matnr-group-page art-full-height flex flex-col gap-4 xl:flex-row">
+ <ElCard class="w-full shrink-0 xl:w-[320px]">
+ <div class="mb-3 flex items-center justify-between gap-3">
+ <div>
+ <div class="text-base font-medium text-[var(--art-text-primary)]">鐗╂枡鍒嗙粍</div>
+ <div class="text-xs text-[var(--art-text-secondary)]">
+ {{ selectedGroupLabel }}
+ </div>
+ </div>
+ <ElButton text @click="handleResetGroup">鍏ㄩ儴</ElButton>
+ </div>
+
+ <div class="mb-3 flex items-center gap-2">
+ <ElInput
+ v-model.trim="groupSearch"
+ clearable
+ placeholder="鎼滅储鐗╂枡鍒嗙粍"
+ @clear="handleGroupSearch"
+ @keyup.enter="handleGroupSearch"
+ />
+ <ElButton @click="handleGroupSearch">鎼滅储</ElButton>
+ </div>
+
+ <ElScrollbar class="h-[calc(100vh-260px)] pr-1">
+ <div v-if="groupTreeLoading" class="py-6">
+ <ElSkeleton :rows="10" animated />
+ </div>
+ <ElEmpty v-else-if="!groupTreeData.length" description="鏆傛棤鐗╂枡鍒嗙粍" />
+ <ElTree
+ v-else
+ :data="groupTreeData"
+ :props="treeProps"
+ node-key="id"
+ highlight-current
+ default-expand-all
+ :current-node-key="selectedGroupId"
+ @node-click="handleGroupNodeClick"
+ >
+ <template #default="{ data }">
+ <div class="flex items-center gap-2">
+ <span class="font-medium">{{ data.name || '--' }}</span>
+ <span class="text-xs text-[var(--art-text-secondary)]">{{ data.code || '--' }}</span>
+ </div>
+ </template>
+ </ElTree>
+ </ElScrollbar>
+ </ElCard>
+
+ <div class="min-w-0 flex-1 space-y-4">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader :loading="loading" v-model:columns="columnChecks" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鍒嗙粍</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <MatnrGroupDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :group-data="currentGroupData"
+ :parent-group-options="parentGroupOptions"
+ :resolve-parent-code="resolveGroupCode"
+ @submit="handleDialogSubmit"
+ />
+
+ <MatnrGroupDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { computed, onMounted, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchDeleteMatnrGroup,
+ fetchExportMatnrGroupReport,
+ fetchGetMatnrGroupDetail,
+ fetchGetMatnrGroupMany,
+ fetchMatnrGroupPage,
+ fetchMatnrGroupTree,
+ fetchSaveMatnrGroup,
+ fetchUpdateMatnrGroup
+ } from '@/api/matnr-group'
+ import MatnrGroupDialog from './modules/matnr-group-dialog.vue'
+ import MatnrGroupDetailDrawer from './modules/matnr-group-detail-drawer.vue'
+ import { createMatnrGroupTableColumns } from './matnrGroupTable.columns'
+ import {
+ MATNR_GROUP_REPORT_STYLE,
+ MATNR_GROUP_REPORT_TITLE,
+ buildMatnrGroupDialogModel,
+ buildMatnrGroupPageQueryParams,
+ buildMatnrGroupPrintRows,
+ buildMatnrGroupReportMeta,
+ buildMatnrGroupSavePayload,
+ buildMatnrGroupTreeLookupMap,
+ buildMatnrGroupTreeQueryParams,
+ createMatnrGroupSearchState,
+ createMatnrGroupTreeSelectOptions,
+ getMatnrGroupPaginationKey,
+ normalizeMatnrGroupDetailRecord,
+ normalizeMatnrGroupListRow,
+ normalizeMatnrGroupTreeRows,
+ resolveMatnrGroupTreeNodeLabel
+ } from './matnrGroupPage.helpers'
+
+ defineOptions({ name: 'MatnrGroup' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createMatnrGroupSearchState())
+ const groupSearch = ref('')
+ const groupTreeLoading = ref(false)
+ const groupTreeData = ref([])
+ const selectedGroupId = ref(null)
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = MATNR_GROUP_REPORT_TITLE
+ const reportQueryParams = computed(() =>
+ buildMatnrGroupPageQueryParams({
+ ...searchForm.value,
+ parentId: selectedGroupId.value ?? ''
+ })
+ )
+
+ const groupLookupMap = computed(() => buildMatnrGroupTreeLookupMap(groupTreeData.value))
+ const parentGroupOptions = computed(() => createMatnrGroupTreeSelectOptions(groupTreeData.value))
+
+ const treeProps = {
+ label: 'displayLabel',
+ children: 'children'
+ }
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ垎缁勭紪鐮�/鍚嶇О/澶囨敞'
+ }
+ },
+ {
+ label: '鍒嗙粍缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ垎缁勭紪鐮�'
+ }
+ },
+ {
+ label: '鍒嗙粍鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ垎缁勫悕绉�'
+ }
+ },
+ {
+ label: '涓婄骇缂栫爜',
+ key: 'parCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ笂绾х紪鐮�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resolveGroupCode(id) {
+ return groupLookupMap.value.get(Number(id))?.code || ''
+ }
+
+ function resolveGroupLabel(id) {
+ const node = groupLookupMap.value.get(Number(id))
+ if (!node) {
+ return ''
+ }
+ return node.displayLabel || resolveMatnrGroupTreeNodeLabel(node)
+ }
+
+ const selectedGroupLabel = computed(() => {
+ if (selectedGroupId.value === null || selectedGroupId.value === undefined) {
+ return '鍏ㄩ儴鍒嗙粍'
+ }
+ return resolveGroupLabel(selectedGroupId.value) || '鍏ㄩ儴鍒嗙粍'
+ })
+
+ async function applyTableFilters() {
+ replaceSearchParams(
+ buildMatnrGroupPageQueryParams({
+ ...searchForm.value,
+ parentId: selectedGroupId.value ?? ''
+ })
+ )
+ await getData()
+ }
+
+ async function loadGroupTree() {
+ groupTreeLoading.value = true
+ let selectionCleared = false
+ try {
+ const records = await guardRequestWithMessage(
+ fetchMatnrGroupTree(buildMatnrGroupTreeQueryParams({ condition: groupSearch.value })),
+ [],
+ { timeoutMessage: '鐗╂枡鍒嗙粍鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ const normalizedTree = normalizeMatnrGroupTreeRows(Array.isArray(records) ? records : [])
+ groupTreeData.value = normalizedTree
+ if (selectedGroupId.value && !groupLookupMap.value.get(Number(selectedGroupId.value))) {
+ selectedGroupId.value = null
+ selectionCleared = true
+ }
+ if (selectionCleared) {
+ await applyTableFilters()
+ }
+ } catch (error) {
+ groupTreeData.value = []
+ ElMessage.error(error?.message || '鐗╂枡鍒嗙粍鍔犺浇澶辫触')
+ } finally {
+ groupTreeLoading.value = false
+ }
+ return selectionCleared
+ }
+
+ async function loadGroupDetail(id) {
+ return guardRequestWithMessage(fetchGetMatnrGroupDetail(id), {}, {
+ timeoutMessage: '鐗╂枡鍒嗙粍璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchMatnrGroupPage,
+ apiParams: buildMatnrGroupPageQueryParams(reportQueryParams.value),
+ paginationKey: getMatnrGroupPaginationKey(),
+ columnsFactory: () =>
+ createMatnrGroupTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ resolveParentLabel: resolveGroupLabel,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeMatnrGroupListRow(item, resolveGroupLabel))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentGroupData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () =>
+ buildMatnrGroupDialogModel({}, {
+ parentId: selectedGroupId.value ?? 0,
+ parCode: resolveGroupCode(selectedGroupId.value ?? 0),
+ resolveParentCode: resolveGroupCode
+ }),
+ buildEditModel: (record) => buildMatnrGroupDialogModel(record, { resolveParentCode: resolveGroupCode }),
+ buildSavePayload: (formData) => buildMatnrGroupSavePayload(formData),
+ saveRequest: (payload) => {
+ if (payload.id !== void 0 && payload.id !== null && Number(payload.parentId) === Number(payload.id)) {
+ throw new Error('涓婄骇鍒嗙粍涓嶈兘閫夋嫨褰撳墠鍒嗙粍')
+ }
+ return fetchSaveMatnrGroup(payload)
+ },
+ updateRequest: (payload) => {
+ if (payload.id !== void 0 && payload.id !== null && Number(payload.parentId) === Number(payload.id)) {
+ throw new Error('涓婄骇鍒嗙粍涓嶈兘閫夋嫨褰撳墠鍒嗙粍')
+ }
+ return fetchUpdateMatnrGroup(payload)
+ },
+ deleteRequest: fetchDeleteMatnrGroup,
+ entityName: '鐗╂枡鍒嗙粍',
+ resolveRecordLabel: (record) => record?.name || record?.code || record?.id,
+ refreshCreate: refreshTreeAndTable,
+ refreshUpdate: refreshTreeAndTable,
+ refreshRemove: refreshTreeAndTable
+ })
+ handleDeleteAction = handleDelete
+
+ async function refreshTreeAndTable() {
+ const selectionCleared = await loadGroupTree()
+ if (!selectionCleared) {
+ await refreshData()
+ }
+ }
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...MATNR_GROUP_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetMatnrGroupMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchMatnrGroupPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'matnr-group.xlsx',
+ requestExport: (payload) =>
+ fetchExportMatnrGroupReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildMatnrGroupPrintRows(records, resolveGroupLabel),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildMatnrGroupReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || MATNR_GROUP_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(
+ buildMatnrGroupPageQueryParams({
+ ...params,
+ parentId: selectedGroupId.value ?? ''
+ })
+ )
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createMatnrGroupSearchState())
+ selectedGroupId.value = null
+ resetSearchParams()
+ }
+
+ async function handleGroupSearch() {
+ await loadGroupTree()
+ }
+
+ async function handleGroupNodeClick(node) {
+ selectedGroupId.value = Number(node?.id || 0) || null
+ await applyTableFilters()
+ }
+
+ async function handleResetGroup() {
+ selectedGroupId.value = null
+ await applyTableFilters()
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await loadGroupDetail(row.id)
+ detailData.value = normalizeMatnrGroupDetailRecord(detail, resolveGroupLabel)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鐗╂枡鍒嗙粍璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await loadGroupDetail(row.id)
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇鐗╂枡鍒嗙粍璇︽儏澶辫触')
+ }
+ }
+
+ onMounted(async () => {
+ await loadGroupTree()
+ await getData()
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/matnr-group/matnrGroupPage.helpers.js b/rsf-design/src/views/basic-info/matnr-group/matnrGroupPage.helpers.js
new file mode 100644
index 0000000..4db6d5c
--- /dev/null
+++ b/rsf-design/src/views/basic-info/matnr-group/matnrGroupPage.helpers.js
@@ -0,0 +1,311 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const MATNR_GROUP_REPORT_TITLE = '鐗╂枡鍒嗙粍鎶ヨ〃'
+export const MATNR_GROUP_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function buildLabel(name, code) {
+ return [normalizeText(name), normalizeText(code)].filter(Boolean).join(' 路 ') || '--'
+}
+
+function normalizeId(value, fallback = void 0) {
+ const numberValue = normalizeNumber(value, fallback)
+ return numberValue === void 0 ? fallback : numberValue
+}
+
+export function createMatnrGroupSearchState() {
+ return {
+ condition: '',
+ code: '',
+ name: '',
+ parCode: '',
+ status: '',
+ memo: '',
+ sort: '',
+ parentId: ''
+ }
+}
+
+export function createMatnrGroupFormState() {
+ return {
+ id: void 0,
+ parentId: 0,
+ parCode: '',
+ code: '',
+ name: '',
+ sort: 0,
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getMatnrGroupPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getMatnrGroupStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getMatnrGroupStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildMatnrGroupPageQueryParams(params = {}) {
+ const result = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+
+ ;['condition', 'code', 'name', 'parCode', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.sort !== '' && params.sort !== null && params.sort !== undefined) {
+ result.sort = Number(params.sort)
+ }
+
+ if (params.status !== '' && params.status !== null && params.status !== undefined) {
+ result.status = Number(params.status)
+ }
+
+ if (params.parentId !== '' && params.parentId !== null && params.parentId !== undefined) {
+ result.parentId = Number(params.parentId)
+ }
+
+ return result
+}
+
+export function buildMatnrGroupTreeQueryParams(params = {}) {
+ return {
+ condition: normalizeText(params.condition)
+ }
+}
+
+export function normalizeMatnrGroupTreeRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records.map((item) => {
+ const children = normalizeMatnrGroupTreeRows(item?.children || [])
+ const id = normalizeId(item?.id)
+ const parentId = normalizeId(item?.parentId, 0)
+ const code = normalizeText(item?.code)
+ const name = normalizeText(item?.name)
+ const label = buildLabel(name, code)
+ const statusMeta = getMatnrGroupStatusMeta(item?.statusBool ?? item?.status)
+
+ return {
+ ...item,
+ id,
+ parentId,
+ code,
+ name,
+ label,
+ displayLabel: label,
+ parCode: normalizeText(item?.parCode),
+ sort: normalizeId(item?.sort, 0),
+ status: normalizeId(item?.status),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ memo: normalizeText(item?.memo) || '-',
+ children
+ }
+ })
+}
+
+export function createMatnrGroupTreeSelectOptions(records = []) {
+ const tree = normalizeMatnrGroupTreeRows(records)
+ return [
+ {
+ id: 0,
+ name: '椤剁骇鍒嗙粍',
+ code: '',
+ label: '椤剁骇鍒嗙粍',
+ displayLabel: '椤剁骇鍒嗙粍',
+ children: tree
+ }
+ ]
+}
+
+export function createMatnrGroupTreeLookupMap(records = []) {
+ const map = new Map()
+
+ const visit = (nodes = []) => {
+ nodes.forEach((node) => {
+ if (!node || typeof node !== 'object') {
+ return
+ }
+ const id = normalizeId(node.id)
+ if (id !== void 0) {
+ map.set(id, node)
+ }
+ if (Array.isArray(node.children) && node.children.length) {
+ visit(node.children)
+ }
+ })
+ }
+
+ visit(records)
+ map.set(0, {
+ id: 0,
+ name: '椤剁骇鍒嗙粍',
+ code: '',
+ label: '椤剁骇鍒嗙粍',
+ displayLabel: '椤剁骇鍒嗙粍'
+ })
+ return map
+}
+
+export function buildMatnrGroupTreeLookupMap(records = []) {
+ return createMatnrGroupTreeLookupMap(records)
+}
+
+export function resolveMatnrGroupTreeNodeLabel(node = {}) {
+ return buildLabel(node?.name, node?.code)
+}
+
+export function buildMatnrGroupDialogModel(record = {}, options = {}) {
+ const resolveParentCode = typeof options.resolveParentCode === 'function' ? options.resolveParentCode : null
+ const parentId =
+ record.parentId !== void 0 && record.parentId !== null && record.parentId !== ''
+ ? normalizeId(record.parentId, 0)
+ : normalizeId(options.parentId, 0)
+ const parentCode =
+ normalizeText(record.parCode) ||
+ normalizeText(options.parCode) ||
+ (resolveParentCode ? normalizeText(resolveParentCode(parentId)) : '')
+
+ return {
+ ...createMatnrGroupFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ parentId,
+ parCode: parentCode,
+ code: normalizeText(record.code || ''),
+ name: normalizeText(record.name || ''),
+ sort:
+ record.sort !== void 0 && record.sort !== null && record.sort !== ''
+ ? Number(record.sort)
+ : 0,
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function buildMatnrGroupSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ parentId:
+ formData.parentId !== void 0 && formData.parentId !== null && formData.parentId !== ''
+ ? Number(formData.parentId)
+ : 0,
+ parCode: normalizeText(formData.parCode) || '',
+ code: normalizeText(formData.code) || '',
+ name: normalizeText(formData.name) || '',
+ sort:
+ formData.sort !== void 0 && formData.sort !== null && formData.sort !== ''
+ ? Number(formData.sort)
+ : 0,
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function normalizeMatnrGroupDetailRecord(record = {}, resolveParentLabel) {
+ const statusMeta = getMatnrGroupStatusMeta(record.statusBool ?? record.status)
+ const parentId = normalizeId(record.parentId, 0)
+ const parentLabel =
+ typeof resolveParentLabel === 'function'
+ ? normalizeText(resolveParentLabel(parentId))
+ : normalizeText(record.parentLabel || record.parentLabelText || '')
+
+ return {
+ ...record,
+ id: normalizeId(record.id),
+ parentId,
+ parentLabel: parentLabel || (parentId === 0 ? '椤剁骇鍒嗙粍' : '--'),
+ code: normalizeText(record.code) || '--',
+ name: normalizeText(record.name) || '--',
+ parCode: normalizeText(record.parCode) || '--',
+ sort: normalizeId(record.sort, 0),
+ status: normalizeId(record.status),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo) || '--',
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeMatnrGroupListRow(record = {}, resolveParentLabel) {
+ return normalizeMatnrGroupDetailRecord(record, resolveParentLabel)
+}
+
+export function buildMatnrGroupPrintRows(records = [], resolveParentLabel) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeMatnrGroupListRow(record, resolveParentLabel))
+}
+
+export function buildMatnrGroupReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = MATNR_GROUP_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: MATNR_GROUP_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...MATNR_GROUP_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/matnr-group/matnrGroupTable.columns.js b/rsf-design/src/views/basic-info/matnr-group/matnrGroupTable.columns.js
new file mode 100644
index 0000000..aaf9e3e
--- /dev/null
+++ b/rsf-design/src/views/basic-info/matnr-group/matnrGroupTable.columns.js
@@ -0,0 +1,131 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getMatnrGroupStatusMeta } from './matnrGroupPage.helpers'
+
+export function createMatnrGroupTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ resolveParentLabel,
+ canEdit = true,
+ canDelete = true
+}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'name',
+ label: '鍒嗙粍鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'code',
+ label: '鍒嗙粍缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'parentLabel',
+ label: '涓婄骇鍒嗙粍',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => {
+ if (typeof resolveParentLabel === 'function') {
+ const resolved = resolveParentLabel(row.parentId)
+ if (resolved) {
+ return resolved
+ }
+ }
+ return row.parentLabel || (Number(row.parentId) === 0 ? '椤剁骇鍒嗙粍' : '--')
+ }
+ },
+ {
+ prop: 'parCode',
+ label: '涓婄骇缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'sort',
+ label: '鎺掑簭',
+ width: 90,
+ align: 'center'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getMatnrGroupStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/matnr-group/modules/matnr-group-detail-drawer.vue b/rsf-design/src/views/basic-info/matnr-group/modules/matnr-group-detail-drawer.vue
new file mode 100644
index 0000000..3659726
--- /dev/null
+++ b/rsf-design/src/views/basic-info/matnr-group/modules/matnr-group-detail-drawer.vue
@@ -0,0 +1,58 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鐗╂枡鍒嗙粍璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒嗙粍鍚嶇О">{{ detail.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒嗙粍缂栫爜">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓婄骇鍒嗙粍">{{ detail.parentLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓婄骇缂栫爜">{{ detail.parCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎺掑簭">{{ detail.sort ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/matnr-group/modules/matnr-group-dialog.vue b/rsf-design/src/views/basic-info/matnr-group/modules/matnr-group-dialog.vue
new file mode 100644
index 0000000..d888371
--- /dev/null
+++ b/rsf-design/src/views/basic-info/matnr-group/modules/matnr-group-dialog.vue
@@ -0,0 +1,202 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="960px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildMatnrGroupDialogModel, createMatnrGroupFormState, getMatnrGroupStatusOptions } from '../matnrGroupPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ groupData: { type: Object, default: () => ({}) },
+ parentGroupOptions: { type: Array, default: () => [] },
+ resolveParentCode: { type: Function, default: null }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createMatnrGroupFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫鐗╂枡鍒嗙粍' : '鏂板鐗╂枡鍒嗙粍'))
+
+ const rules = computed(() => ({
+ parentId: [{ required: true, message: '璇烽�夋嫨涓婄骇鍒嗙粍', trigger: 'change' }],
+ code: [{ required: true, message: '璇疯緭鍏ュ垎缁勭紪鐮�', trigger: 'blur' }],
+ name: [{ required: true, message: '璇疯緭鍏ュ垎缁勫悕绉�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '涓婄骇鍒嗙粍',
+ key: 'parentId',
+ type: 'treeselect',
+ span: 24,
+ props: {
+ data: props.parentGroupOptions,
+ props: {
+ label: 'displayLabel',
+ value: 'id',
+ children: 'children'
+ },
+ placeholder: '璇烽�夋嫨涓婄骇鍒嗙粍',
+ clearable: false,
+ checkStrictly: true,
+ defaultExpandAll: true,
+ onChange: handleParentChange
+ }
+ },
+ {
+ label: '涓婄骇缂栫爜',
+ key: 'parCode',
+ type: 'input',
+ props: {
+ placeholder: '鑷姩甯﹀嚭涓婄骇缂栫爜',
+ clearable: true,
+ readonly: true
+ }
+ },
+ {
+ label: '鍒嗙粍缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ垎缁勭紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍒嗙粍鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ垎缁勫悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '鎺掑簭',
+ key: 'sort',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getMatnrGroupStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const resetForm = () => {
+ Object.assign(form, createMatnrGroupFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const loadFormData = () => {
+ Object.assign(
+ form,
+ buildMatnrGroupDialogModel(props.groupData, {
+ parentId: props.groupData?.parentId ?? 0,
+ parCode: props.groupData?.parCode || '',
+ resolveParentCode: props.resolveParentCode
+ })
+ )
+ }
+
+ function handleParentChange(value) {
+ if (typeof props.resolveParentCode === 'function') {
+ form.parCode = String(props.resolveParentCode(value) || '').trim()
+ }
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.groupData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template-merge/index.vue b/rsf-design/src/views/basic-info/task-path-template-merge/index.vue
new file mode 100644
index 0000000..7203b7f
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-merge/index.vue
@@ -0,0 +1,323 @@
+<template>
+ <div class="task-path-template-merge-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鍚堝苟</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <TaskPathTemplateMergeDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :task-path-template-merge-data="currentTaskPathTemplateMergeData"
+ :type-options="typeOptions"
+ :template-options="templateOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <TaskPathTemplateMergeDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchTaskPathTemplateList } from '@/api/task-path-template'
+ import {
+ fetchDeleteTaskPathTemplateMerge,
+ fetchExportTaskPathTemplateMergeReport,
+ fetchGetTaskPathTemplateMergeDetail,
+ fetchGetTaskPathTemplateMergeMany,
+ fetchSaveTaskPathTemplateMerge,
+ fetchTaskPathTemplateMergeCreateSelectList,
+ fetchTaskPathTemplateMergePage,
+ fetchUpdateTaskPathTemplateMerge
+ } from '@/api/task-path-template-merge'
+ import TaskPathTemplateMergeDialog from './modules/task-path-template-merge-dialog.vue'
+ import TaskPathTemplateMergeDetailDrawer from './modules/task-path-template-merge-detail-drawer.vue'
+ import { createTaskPathTemplateMergeTableColumns } from './taskPathTemplateMergeTable.columns'
+ import {
+ TASK_PATH_TEMPLATE_MERGE_REPORT_STYLE,
+ TASK_PATH_TEMPLATE_MERGE_REPORT_TITLE,
+ buildTaskPathTemplateMergeConditionOptions,
+ buildTaskPathTemplateMergeDialogModel,
+ buildTaskPathTemplateMergePageQueryParams,
+ buildTaskPathTemplateMergePrintRows,
+ buildTaskPathTemplateMergeReportMeta,
+ buildTaskPathTemplateMergeSavePayload,
+ buildTaskPathTemplateMergeSearchParams,
+ buildTaskPathTemplateMergeTypeOptions,
+ createTaskPathTemplateMergeSearchState,
+ getTaskPathTemplateMergePaginationKey,
+ normalizeTaskPathTemplateMergeListRow
+ } from './taskPathTemplateMergePage.helpers'
+
+ defineOptions({ name: 'TaskPathTemplateMerge' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createTaskPathTemplateMergeSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const typeOptions = ref([])
+ const templateOptions = ref([])
+ let handleDeleteAction = null
+
+ const reportTitle = TASK_PATH_TEMPLATE_MERGE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildTaskPathTemplateMergeSearchParams(searchForm.value))
+
+ const typeLabelMap = computed(() => new Map(typeOptions.value.map((item) => [String(item.value), item.label])))
+ const templateLabelMap = computed(
+ () => new Map(templateOptions.value.map((item) => [String(item.value), item.label]))
+ )
+
+ function resolveTypeLabel(value) {
+ return typeLabelMap.value.get(String(value)) || String(value ?? '')
+ }
+
+ function resolveTemplateLabel(value) {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => templateLabelMap.value.get(String(item)) || String(item ?? ''))
+ .filter(Boolean)
+ .join('銆�')
+ }
+ return templateLabelMap.value.get(String(value)) || String(value ?? '')
+ }
+
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユā鏉跨紪鐮�/鍚嶇О/鏉′欢鎻忚堪' } },
+ { label: '妯℃澘缂栫爜', key: 'templateCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユā鏉跨紪鐮�' } },
+ { label: '妯℃澘鍚嶇О', key: 'templateName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユā鏉垮悕绉�' } },
+ { label: '璧风偣绫诲瀷', key: 'sourceType', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ捣鐐圭被鍨�' } },
+ { label: '缁堢偣绫诲瀷', key: 'targetType', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ粓鐐圭被鍨�' } },
+ { label: '鏉′欢琛ㄨ揪寮�', key: 'conditionExpression', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ潯浠惰〃杈惧紡' } },
+ { label: '鏉′欢鎻忚堪', key: 'conditionDesc', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ潯浠舵弿杩�' } },
+ { label: '鐗堟湰鍙�', key: 'version', type: 'number', props: { min: 1, controlsPosition: 'right', placeholder: '璇疯緭鍏ョ増鏈彿' } },
+ { label: '褰撳墠鐗堟湰', key: 'isCurrent', type: 'select', props: { clearable: true, options: [{ label: '褰撳墠鐗堟湰', value: 1 }, { label: '鍘嗗彶鐗堟湰', value: 0 }] } },
+ { label: '浼樺厛绾�', key: 'priority', type: 'number', props: { min: 1, controlsPosition: 'right', placeholder: '璇疯緭鍏ヤ紭鍏堢骇' } },
+ { label: '瓒呮椂(鍒�)', key: 'timeoutMinutes', type: 'number', props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ヨ秴鏃舵椂闂�' } },
+ { label: '鏈�澶ч噸璇�', key: 'maxRetryTimes', type: 'number', props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ユ渶澶ч噸璇曟鏁�' } },
+ { label: '閲嶈瘯闂撮殧(绉�)', key: 'retryIntervalSeconds', type: 'number', props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ラ噸璇曢棿闅�' } },
+ { label: '姝ュ簭闀垮害', key: 'stepSize', type: 'number', props: { min: 1, controlsPosition: 'right', placeholder: '璇疯緭鍏ユ搴忛暱搴�' } },
+ { label: '鐘舵��', key: 'status', type: 'select', props: { clearable: true, options: [{ label: '鍚敤', value: 1 }, { label: '绂佺敤', value: 0 }] } },
+ { label: '鐢熸晥鏃堕棿', key: 'effectiveTime', type: 'datetime', props: { type: 'datetime', valueFormat: 'YYYY-MM-DD HH:mm:ss', format: 'YYYY-MM-DD HH:mm:ss', placeholder: '璇烽�夋嫨鐢熸晥鏃堕棿' } },
+ { label: '澶辨晥鏃堕棿', key: 'expireTime', type: 'datetime', props: { type: 'datetime', valueFormat: 'YYYY-MM-DD HH:mm:ss', format: 'YYYY-MM-DD HH:mm:ss', placeholder: '璇烽�夋嫨澶辨晥鏃堕棿' } }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetTaskPathTemplateMergeDetail(row.id), {}, {
+ timeoutMessage: '浠诲姟璺緞妯℃澘鍚堝苟璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeTaskPathTemplateMergeListRow(detail, resolveTypeLabel, templateLabelMap.value)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇浠诲姟璺緞妯℃澘鍚堝苟璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetTaskPathTemplateMergeDetail(row.id), {}, {
+ timeoutMessage: '浠诲姟璺緞妯℃澘鍚堝苟璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇浠诲姟璺緞妯℃澘鍚堝苟璇︽儏澶辫触')
+ }
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchTaskPathTemplateMergePage,
+ apiParams: buildTaskPathTemplateMergePageQueryParams(searchForm.value),
+ paginationKey: getTaskPathTemplateMergePaginationKey(),
+ columnsFactory: () =>
+ createTaskPathTemplateMergeTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ resolveTypeLabel,
+ resolveTemplateLabel,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) =>
+ normalizeTaskPathTemplateMergeListRow(item, resolveTypeLabel, templateLabelMap.value)
+ )
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentTaskPathTemplateMergeData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildTaskPathTemplateMergeDialogModel({}),
+ buildEditModel: (record) => buildTaskPathTemplateMergeDialogModel(record),
+ buildSavePayload: (formData) => buildTaskPathTemplateMergeSavePayload(formData),
+ saveRequest: fetchSaveTaskPathTemplateMerge,
+ updateRequest: fetchUpdateTaskPathTemplateMerge,
+ deleteRequest: fetchDeleteTaskPathTemplateMerge,
+ entityName: '浠诲姟璺緞妯℃澘鍚堝苟',
+ resolveRecordLabel: (record) => record?.templateCode || record?.templateName || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...TASK_PATH_TEMPLATE_MERGE_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetTaskPathTemplateMergeMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchTaskPathTemplateMergePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'task-path-template-merge.xlsx',
+ requestExport: (payload) =>
+ fetchExportTaskPathTemplateMergeReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTaskPathTemplateMergePrintRows(records, resolveTypeLabel, templateLabelMap.value),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildTaskPathTemplateMergeReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: TASK_PATH_TEMPLATE_MERGE_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTaskPathTemplateMergeSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTaskPathTemplateMergeSearchState())
+ resetSearchParams()
+ }
+
+ async function loadTypeOptions() {
+ const response = await guardRequestWithMessage(fetchTaskPathTemplateMergeCreateSelectList(), [], {
+ timeoutMessage: '璧风偣/缁堢偣绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ typeOptions.value = buildTaskPathTemplateMergeTypeOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadTemplateOptions() {
+ const response = await guardRequestWithMessage(fetchTaskPathTemplateList(), [], {
+ timeoutMessage: '鏉′欢琛ㄨ揪寮忔ā鏉垮姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ templateOptions.value = buildTaskPathTemplateMergeConditionOptions(defaultResponseAdapter(response).records)
+ }
+
+ onMounted(async () => {
+ await Promise.all([loadTypeOptions(), loadTemplateOptions(), getData()])
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template-merge/modules/task-path-template-merge-detail-drawer.vue b/rsf-design/src/views/basic-info/task-path-template-merge/modules/task-path-template-merge-detail-drawer.vue
new file mode 100644
index 0000000..447b123
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-merge/modules/task-path-template-merge-detail-drawer.vue
@@ -0,0 +1,83 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟璺緞妯℃澘鍚堝苟璇︽儏"
+ size="920px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="10" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElCard shadow="never" class="art-table-card">
+ <template #header>
+ <div class="flex items-center justify-between gap-3">
+ <div>
+ <h3 class="m-0 text-base font-semibold">
+ {{ detail.templateCode || detail.templateName || '浠诲姟璺緞妯℃澘鍚堝苟' }}
+ </h3>
+ <p class="m-0 text-sm text-[var(--art-text-secondary)]">
+ 杩欓噷灞曠ず鐨勬槸鍚庣鐪熷疄璁板綍鐨勭粍鍚堣鎯咃紝涓嶅仛棰濆鎺ㄦ紨銆�
+ </p>
+ </div>
+ <ElSpace wrap>
+ <ElTag :type="detail.isCurrentType || 'info'" effect="light">
+ {{ detail.isCurrentText || '--' }}
+ </ElTag>
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElSpace>
+ </div>
+ </template>
+
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="妯℃澘缂栫爜">{{ detail.templateCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="妯℃澘鍚嶇О">{{ detail.templateName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璧风偣绫诲瀷">{{ detail.sourceTypeText || detail.sourceType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁堢偣绫诲瀷">{{ detail.targetTypeText || detail.targetType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉′欢琛ㄨ揪寮�">{{ detail.conditionExpressionText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉′欢鎻忚堪">{{ detail.conditionDesc || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗堟湰鍙�">{{ detail.version ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浼樺厛绾�">{{ detail.priority ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="褰撳墠鐗堟湰">{{ detail.isCurrentText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐢熸晥鏃堕棿">{{ detail.effectiveTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶辨晥鏃堕棿">{{ detail.expireTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瓒呮椂(鍒�)">{{ detail.timeoutMinutes ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�澶ч噸璇�">{{ detail.maxRetryTimes ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲嶈瘯闂撮殧(绉�)">{{ detail.retryIntervalSeconds ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ュ簭闀垮害">{{ detail.stepSize ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElCard>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template-merge/modules/task-path-template-merge-dialog.vue b/rsf-design/src/views/basic-info/task-path-template-merge/modules/task-path-template-merge-dialog.vue
new file mode 100644
index 0000000..5616749
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-merge/modules/task-path-template-merge-dialog.vue
@@ -0,0 +1,320 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="1040px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="120px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildTaskPathTemplateMergeDialogModel,
+ createTaskPathTemplateMergeFormState,
+ getTaskPathTemplateMergeCurrentOptions,
+ getTaskPathTemplateMergeStatusOptions
+ } from '../taskPathTemplateMergePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ taskPathTemplateMergeData: { type: Object, default: () => ({}) },
+ typeOptions: { type: Array, default: () => [] },
+ templateOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createTaskPathTemplateMergeFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫浠诲姟璺緞妯℃澘鍚堝苟' : '鏂板浠诲姟璺緞妯℃澘鍚堝苟'))
+
+ const rules = computed(() => {
+ const baseRules = {
+ conditionExpression: [{ required: true, message: '璇烽�夋嫨鏉′欢琛ㄨ揪寮�', trigger: 'change' }],
+ conditionDesc: [{ required: true, message: '璇疯緭鍏ユ潯浠舵弿杩�', trigger: 'blur' }],
+ version: [{ required: true, message: '璇疯緭鍏ョ増鏈彿', trigger: 'blur' }],
+ isCurrent: [{ required: true, message: '璇烽�夋嫨鏄惁褰撳墠鐗堟湰', trigger: 'change' }],
+ effectiveTime: [{ required: true, message: '璇烽�夋嫨鐢熸晥鏃堕棿', trigger: 'change' }],
+ priority: [{ required: true, message: '璇疯緭鍏ヤ紭鍏堢骇', trigger: 'blur' }],
+ status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }]
+ }
+
+ if (isEdit.value) {
+ baseRules.sourceType = [{ required: true, message: '璇烽�夋嫨璧风偣绫诲瀷', trigger: 'change' }]
+ baseRules.targetType = [{ required: true, message: '璇烽�夋嫨缁堢偣绫诲瀷', trigger: 'change' }]
+ } else {
+ baseRules.sourceTypeR = [{ type: 'array', required: true, message: '璇烽�夋嫨璧风偣绫诲瀷', trigger: 'change' }]
+ baseRules.targetTypeR = [{ type: 'array', required: true, message: '璇烽�夋嫨缁堢偣绫诲瀷', trigger: 'change' }]
+ }
+
+ return baseRules
+ })
+
+ const commonItems = () => [
+ {
+ label: '鏉′欢琛ㄨ揪寮�',
+ key: 'conditionExpression',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨鏉′欢琛ㄨ揪寮�',
+ clearable: true,
+ filterable: true,
+ options: props.templateOptions || []
+ }
+ },
+ {
+ label: '鏉′欢鎻忚堪',
+ key: 'conditionDesc',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ユ潯浠舵弿杩�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐗堟湰鍙�',
+ key: 'version',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ増鏈彿'
+ }
+ },
+ {
+ label: '鏄惁褰撳墠鐗堟湰',
+ key: 'isCurrent',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏄惁褰撳墠鐗堟湰',
+ clearable: true,
+ options: getTaskPathTemplateMergeCurrentOptions()
+ }
+ },
+ {
+ label: '鐢熸晥鏃堕棿',
+ key: 'effectiveTime',
+ type: 'datetime',
+ props: {
+ type: 'datetime',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨鐢熸晥鏃堕棿'
+ }
+ },
+ {
+ label: '澶辨晥鏃堕棿',
+ key: 'expireTime',
+ type: 'datetime',
+ props: {
+ type: 'datetime',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨澶辨晥鏃堕棿'
+ }
+ },
+ {
+ label: '浼樺厛绾�',
+ key: 'priority',
+ type: 'number',
+ props: {
+ min: 1,
+ max: 99,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ紭鍏堢骇'
+ }
+ },
+ {
+ label: '瓒呮椂(鍒�)',
+ key: 'timeoutMinutes',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ秴鏃舵椂闂�'
+ }
+ },
+ {
+ label: '鏈�澶ч噸璇曟鏁�',
+ key: 'maxRetryTimes',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ渶澶ч噸璇曟鏁�'
+ }
+ },
+ {
+ label: '閲嶈瘯闂撮殧(绉�)',
+ key: 'retryIntervalSeconds',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ラ噸璇曢棿闅�'
+ }
+ },
+ {
+ label: '姝ュ簭闀垮害',
+ key: 'stepSize',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ搴忛暱搴�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getTaskPathTemplateMergeStatusOptions()
+ }
+ }
+ ]
+
+ const formItems = computed(() => {
+ if (isEdit.value) {
+ return [
+ {
+ label: '璧风偣绫诲瀷',
+ key: 'sourceType',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨璧风偣绫诲瀷',
+ clearable: true,
+ filterable: true,
+ options: props.typeOptions || []
+ }
+ },
+ {
+ label: '缁堢偣绫诲瀷',
+ key: 'targetType',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨缁堢偣绫诲瀷',
+ clearable: true,
+ filterable: true,
+ options: props.typeOptions || []
+ }
+ },
+ ...commonItems()
+ ]
+ }
+
+ return [
+ {
+ label: '璧风偣绫诲瀷',
+ key: 'sourceTypeR',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨璧风偣绫诲瀷',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.typeOptions || []
+ }
+ },
+ {
+ label: '缁堢偣绫诲瀷',
+ key: 'targetTypeR',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨缁堢偣绫诲瀷',
+ clearable: true,
+ multiple: true,
+ collapseTags: true,
+ filterable: true,
+ options: props.typeOptions || []
+ }
+ },
+ ...commonItems()
+ ]
+ })
+
+ const loadFormData = () => {
+ Object.assign(form, buildTaskPathTemplateMergeDialogModel(props.taskPathTemplateMergeData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createTaskPathTemplateMergeFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.taskPathTemplateMergeData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template-merge/taskPathTemplateMergePage.helpers.js b/rsf-design/src/views/basic-info/task-path-template-merge/taskPathTemplateMergePage.helpers.js
new file mode 100644
index 0000000..965931a
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-merge/taskPathTemplateMergePage.helpers.js
@@ -0,0 +1,484 @@
+const STATUS_META = {
+ 1: { text: '鍚敤', type: 'success', bool: true },
+ 0: { text: '绂佺敤', type: 'danger', bool: false }
+}
+
+const CURRENT_META = {
+ 1: { text: '褰撳墠鐗堟湰', type: 'success', bool: true },
+ 0: { text: '鍘嗗彶鐗堟湰', type: 'info', bool: false }
+}
+
+export const TASK_PATH_TEMPLATE_MERGE_REPORT_TITLE = '浠诲姟璺緞妯℃澘鍚堝苟鎶ヨ〃'
+export const TASK_PATH_TEMPLATE_MERGE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeStringList(value) {
+ if (Array.isArray(value)) {
+ return value.map((item) => normalizeText(item)).filter(Boolean)
+ }
+ if (value === null || value === undefined || value === '') {
+ return []
+ }
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text) {
+ return []
+ }
+ if (text.startsWith('[')) {
+ try {
+ const parsed = JSON.parse(text)
+ if (Array.isArray(parsed)) {
+ return parsed.map((item) => normalizeText(item)).filter(Boolean)
+ }
+ } catch {
+ return [text]
+ }
+ }
+ return text.split(/[;,锛孿n\r]+/g).map((item) => item.trim()).filter(Boolean)
+ }
+ return [normalizeText(value)].filter(Boolean)
+}
+
+function normalizeIntegerList(value) {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => {
+ if (item === null || item === undefined || item === '') {
+ return null
+ }
+ const numberValue = Number(item)
+ return Number.isNaN(numberValue) ? null : numberValue
+ })
+ .filter((item) => item !== null)
+ }
+ if (value === null || value === undefined || value === '') {
+ return []
+ }
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text) {
+ return []
+ }
+ if (text.startsWith('[')) {
+ try {
+ const parsed = JSON.parse(text)
+ if (Array.isArray(parsed)) {
+ return parsed
+ .map((item) => {
+ const numberValue = Number(item)
+ return Number.isNaN(numberValue) ? null : numberValue
+ })
+ .filter((item) => item !== null)
+ }
+ } catch {
+ return text
+ .split(/[;,锛孿n\r]+/g)
+ .map((item) => Number(item.trim()))
+ .filter((item) => !Number.isNaN(item))
+ }
+ }
+ const numberValue = Number(text)
+ return Number.isNaN(numberValue) ? [] : [numberValue]
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? [] : [numberValue]
+}
+
+export function createTaskPathTemplateMergeSearchState() {
+ return {
+ condition: '',
+ templateCode: '',
+ templateName: '',
+ sourceType: '',
+ targetType: '',
+ conditionExpression: '',
+ conditionDesc: '',
+ version: '',
+ isCurrent: '',
+ effectiveTime: '',
+ expireTime: '',
+ priority: '',
+ timeoutMinutes: '',
+ maxRetryTimes: '',
+ retryIntervalSeconds: '',
+ stepSize: '',
+ status: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createTaskPathTemplateMergeFormState() {
+ return {
+ id: void 0,
+ templateCode: '',
+ templateName: '',
+ sourceTypeR: [],
+ targetTypeR: [],
+ sourceType: '',
+ targetType: '',
+ conditionExpression: '',
+ conditionDesc: '',
+ version: 1,
+ isCurrent: 1,
+ effectiveTime: '',
+ expireTime: '',
+ priority: 1,
+ timeoutMinutes: '',
+ maxRetryTimes: 3,
+ retryIntervalSeconds: 60,
+ stepSize: '',
+ status: 1
+ }
+}
+
+export function getTaskPathTemplateMergePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getTaskPathTemplateMergeStatusOptions() {
+ return [
+ { label: '鍚敤', value: 1 },
+ { label: '绂佺敤', value: 0 }
+ ]
+}
+
+export function getTaskPathTemplateMergeCurrentOptions() {
+ return [
+ { label: '褰撳墠鐗堟湰', value: 1 },
+ { label: '鍘嗗彶鐗堟湰', value: 0 }
+ ]
+}
+
+export function getTaskPathTemplateMergeStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function getTaskPathTemplateMergeCurrentMeta(isCurrent) {
+ if (isCurrent === true || Number(isCurrent) === 1) {
+ return CURRENT_META[1]
+ }
+ if (isCurrent === false || Number(isCurrent) === 0) {
+ return CURRENT_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildTaskPathTemplateMergeTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: String(value),
+ label: normalizeText(item.name || item.label || `绫诲瀷 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function buildTaskPathTemplateMergeConditionOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.id ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ value: Number(value),
+ label: normalizeText(item.templateName || item.templateCode || item.name || `妯℃澘 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function buildTaskPathTemplateMergeSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ templateCode: normalizeText(params.templateCode),
+ templateName: normalizeText(params.templateName),
+ sourceType: normalizeText(params.sourceType),
+ targetType: normalizeText(params.targetType),
+ conditionExpression: normalizeText(params.conditionExpression),
+ conditionDesc: normalizeText(params.conditionDesc),
+ version:
+ params.version !== undefined && params.version !== null && params.version !== ''
+ ? Number(params.version)
+ : void 0,
+ isCurrent:
+ params.isCurrent !== undefined && params.isCurrent !== null && params.isCurrent !== ''
+ ? Number(params.isCurrent)
+ : void 0,
+ priority:
+ params.priority !== undefined && params.priority !== null && params.priority !== ''
+ ? Number(params.priority)
+ : void 0,
+ timeoutMinutes:
+ params.timeoutMinutes !== undefined && params.timeoutMinutes !== null && params.timeoutMinutes !== ''
+ ? Number(params.timeoutMinutes)
+ : void 0,
+ maxRetryTimes:
+ params.maxRetryTimes !== undefined && params.maxRetryTimes !== null && params.maxRetryTimes !== ''
+ ? Number(params.maxRetryTimes)
+ : void 0,
+ retryIntervalSeconds:
+ params.retryIntervalSeconds !== undefined && params.retryIntervalSeconds !== null && params.retryIntervalSeconds !== ''
+ ? Number(params.retryIntervalSeconds)
+ : void 0,
+ stepSize:
+ params.stepSize !== undefined && params.stepSize !== null && params.stepSize !== ''
+ ? Number(params.stepSize)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ effectiveTime: normalizeText(params.effectiveTime),
+ expireTime: normalizeText(params.expireTime),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildTaskPathTemplateMergePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTaskPathTemplateMergeSearchParams(params)
+ }
+}
+
+export function buildTaskPathTemplateMergeSavePayload(formData = {}) {
+ const isEditMode = formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ const sourceTypeR = normalizeStringList(formData.sourceTypeR)
+ const targetTypeR = normalizeStringList(formData.targetTypeR)
+ const sourceType = normalizeText(formData.sourceType || '')
+ const targetType = normalizeText(formData.targetType || '')
+ const conditionExpression = normalizeIntegerList(formData.conditionExpression)
+ const derivedSource = sourceType || sourceTypeR[0] || ''
+ const derivedTarget = targetType || targetTypeR[0] || ''
+ const templateName =
+ normalizeText(formData.templateName || '') || (derivedSource && derivedTarget ? `${derivedSource}==>${derivedTarget}` : '')
+ const templateCode = normalizeText(formData.templateCode || '') || templateName
+
+ const payload = {
+ ...(isEditMode ? { id: Number(formData.id) } : {}),
+ templateCode,
+ templateName,
+ conditionExpression,
+ conditionDesc: normalizeText(formData.conditionDesc || ''),
+ version:
+ formData.version !== void 0 && formData.version !== null && formData.version !== ''
+ ? Number(formData.version)
+ : 1,
+ isCurrent:
+ formData.isCurrent !== void 0 && formData.isCurrent !== null && formData.isCurrent !== ''
+ ? Number(formData.isCurrent)
+ : 1,
+ effectiveTime: normalizeText(formData.effectiveTime || ''),
+ expireTime: normalizeText(formData.expireTime || ''),
+ priority:
+ formData.priority !== void 0 && formData.priority !== null && formData.priority !== ''
+ ? Number(formData.priority)
+ : 1,
+ ...(formData.timeoutMinutes !== void 0 && formData.timeoutMinutes !== null && formData.timeoutMinutes !== ''
+ ? { timeoutMinutes: Number(formData.timeoutMinutes) }
+ : {}),
+ maxRetryTimes:
+ formData.maxRetryTimes !== void 0 && formData.maxRetryTimes !== null && formData.maxRetryTimes !== ''
+ ? Number(formData.maxRetryTimes)
+ : 3,
+ retryIntervalSeconds:
+ formData.retryIntervalSeconds !== void 0 &&
+ formData.retryIntervalSeconds !== null &&
+ formData.retryIntervalSeconds !== ''
+ ? Number(formData.retryIntervalSeconds)
+ : 60,
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ ...(formData.stepSize !== void 0 && formData.stepSize !== null && formData.stepSize !== ''
+ ? { stepSize: Number(formData.stepSize) }
+ : {})
+ }
+
+ if (isEditMode) {
+ payload.sourceType = sourceType
+ payload.targetType = targetType
+ } else {
+ payload.sourceTypeR = sourceTypeR
+ payload.targetTypeR = targetTypeR
+ }
+
+ return payload
+}
+
+export function buildTaskPathTemplateMergeDialogModel(record = {}) {
+ const sourceTypeR = normalizeStringList(record.sourceTypeR)
+ const targetTypeR = normalizeStringList(record.targetTypeR)
+ const conditionExpression = normalizeIntegerList(record.conditionExpression)
+
+ return {
+ ...createTaskPathTemplateMergeFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ templateCode: normalizeText(record.templateCode || ''),
+ templateName: normalizeText(record.templateName || ''),
+ sourceTypeR,
+ targetTypeR,
+ sourceType: normalizeText(record.sourceType || ''),
+ targetType: normalizeText(record.targetType || ''),
+ conditionExpression: conditionExpression.length ? conditionExpression[0] : '',
+ conditionDesc: normalizeText(record.conditionDesc || ''),
+ version: normalizeNumber(record.version, 1),
+ isCurrent: normalizeNumber(record.isCurrent, 1),
+ effectiveTime: normalizeText(record.effectiveTime$ || record.effectiveTime || ''),
+ expireTime: normalizeText(record.expireTime$ || record.expireTime || ''),
+ priority: normalizeNumber(record.priority, 1),
+ timeoutMinutes: normalizeNumber(record.timeoutMinutes, ''),
+ maxRetryTimes: normalizeNumber(record.maxRetryTimes, 3),
+ retryIntervalSeconds: normalizeNumber(record.retryIntervalSeconds, 60),
+ stepSize: normalizeNumber(record.stepSize, ''),
+ status: normalizeNumber(record.status, 1)
+ }
+}
+
+function resolveConditionExpressionText(value, templateOptionsMap) {
+ const list = normalizeIntegerList(value)
+ if (!list.length) {
+ return ''
+ }
+ return list
+ .map((item) => templateOptionsMap?.get(String(item)) || templateOptionsMap?.get(Number(item)) || String(item))
+ .filter(Boolean)
+ .join('銆�')
+}
+
+export function normalizeTaskPathTemplateMergeDetailRecord(
+ record = {},
+ resolveTypeLabel,
+ templateOptionsMap
+) {
+ const sourceType = normalizeText(record.sourceType || '')
+ const targetType = normalizeText(record.targetType || '')
+ const conditionExpression = normalizeIntegerList(record.conditionExpression)
+ const statusMeta = getTaskPathTemplateMergeStatusMeta(record.statusBool ?? record.status)
+ const currentMeta = getTaskPathTemplateMergeCurrentMeta(record.isCurrent)
+ const sourceTypeText =
+ normalizeText(record.sourceType$ || record.sourceTypeText || '') ||
+ (typeof resolveTypeLabel === 'function' ? normalizeText(resolveTypeLabel(sourceType)) : '')
+ const targetTypeText =
+ normalizeText(record.targetType$ || record.targetTypeText || '') ||
+ (typeof resolveTypeLabel === 'function' ? normalizeText(resolveTypeLabel(targetType)) : '')
+ const conditionExpressionText =
+ normalizeText(record.conditionExpressionText || '') ||
+ resolveConditionExpressionText(conditionExpression, templateOptionsMap)
+
+ return {
+ ...record,
+ templateCode: normalizeText(record.templateCode || ''),
+ templateName: normalizeText(record.templateName || ''),
+ sourceType,
+ targetType,
+ sourceTypeText: sourceTypeText || sourceType || '--',
+ targetTypeText: targetTypeText || targetType || '--',
+ conditionExpression,
+ conditionExpressionText: conditionExpressionText || '--',
+ conditionDesc: normalizeText(record.conditionDesc || ''),
+ version: normalizeNumber(record.version, void 0),
+ isCurrent: normalizeNumber(record.isCurrent, void 0),
+ priority: normalizeNumber(record.priority, void 0),
+ timeoutMinutes: normalizeNumber(record.timeoutMinutes, void 0),
+ maxRetryTimes: normalizeNumber(record.maxRetryTimes, void 0),
+ retryIntervalSeconds: normalizeNumber(record.retryIntervalSeconds, void 0),
+ stepSize: normalizeNumber(record.stepSize, void 0),
+ status: normalizeNumber(record.status, void 0),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ isCurrentText: currentMeta.text,
+ isCurrentType: currentMeta.type,
+ isCurrentBool: record.isCurrentBool !== void 0 ? Boolean(record.isCurrentBool) : currentMeta.bool,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ effectiveTimeText: normalizeText(record.effectiveTime$ || record.effectiveTime || ''),
+ expireTimeText: normalizeText(record.expireTime$ || record.expireTime || '')
+ }
+}
+
+export function normalizeTaskPathTemplateMergeListRow(record = {}, resolveTypeLabel, templateOptionsMap) {
+ return normalizeTaskPathTemplateMergeDetailRecord(record, resolveTypeLabel, templateOptionsMap)
+}
+
+export function buildTaskPathTemplateMergePrintRows(records = [], resolveTypeLabel, templateOptionsMap) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeTaskPathTemplateMergeListRow(record, resolveTypeLabel, templateOptionsMap))
+}
+
+export function buildTaskPathTemplateMergeReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = TASK_PATH_TEMPLATE_MERGE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: TASK_PATH_TEMPLATE_MERGE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...TASK_PATH_TEMPLATE_MERGE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/task-path-template-merge/taskPathTemplateMergeTable.columns.js b/rsf-design/src/views/basic-info/task-path-template-merge/taskPathTemplateMergeTable.columns.js
new file mode 100644
index 0000000..c52aaa6
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-merge/taskPathTemplateMergeTable.columns.js
@@ -0,0 +1,200 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import {
+ getTaskPathTemplateMergeCurrentMeta,
+ getTaskPathTemplateMergeStatusMeta
+} from './taskPathTemplateMergePage.helpers'
+
+export function createTaskPathTemplateMergeTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ resolveTypeLabel,
+ resolveTemplateLabel,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'templateCode',
+ label: '妯℃澘缂栫爜',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.templateCode || '--'
+ },
+ {
+ prop: 'templateName',
+ label: '妯℃澘鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.templateName || '--'
+ },
+ {
+ prop: 'sourceTypeText',
+ label: '璧风偣绫诲瀷',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) =>
+ row.sourceTypeText || (typeof resolveTypeLabel === 'function' ? resolveTypeLabel(row.sourceType) : '') || '--'
+ },
+ {
+ prop: 'targetTypeText',
+ label: '缁堢偣绫诲瀷',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) =>
+ row.targetTypeText || (typeof resolveTypeLabel === 'function' ? resolveTypeLabel(row.targetType) : '') || '--'
+ },
+ {
+ prop: 'conditionExpressionText',
+ label: '鏉′欢琛ㄨ揪寮�',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) =>
+ row.conditionExpressionText ||
+ (typeof resolveTemplateLabel === 'function' ? resolveTemplateLabel(row.conditionExpression) : '') ||
+ '--'
+ },
+ {
+ prop: 'conditionDesc',
+ label: '鏉′欢鎻忚堪',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.conditionDesc || '--'
+ },
+ {
+ prop: 'version',
+ label: '鐗堟湰鍙�',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.version ?? '--'
+ },
+ {
+ prop: 'isCurrent',
+ label: '褰撳墠鐗堟湰',
+ width: 110,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getTaskPathTemplateMergeCurrentMeta(row.isCurrentBool ?? row.isCurrent)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'effectiveTimeText',
+ label: '鐢熸晥鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.effectiveTimeText || '--'
+ },
+ {
+ prop: 'expireTimeText',
+ label: '澶辨晥鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.expireTimeText || '--'
+ },
+ {
+ prop: 'priority',
+ label: '浼樺厛绾�',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.priority ?? '--'
+ },
+ {
+ prop: 'timeoutMinutes',
+ label: '瓒呮椂(鍒�)',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.timeoutMinutes ?? '--'
+ },
+ {
+ prop: 'maxRetryTimes',
+ label: '鏈�澶ч噸璇�',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.maxRetryTimes ?? '--'
+ },
+ {
+ prop: 'retryIntervalSeconds',
+ label: '閲嶈瘯闂撮殧(绉�)',
+ minWidth: 120,
+ align: 'center',
+ formatter: (row) => row.retryIntervalSeconds ?? '--'
+ },
+ {
+ prop: 'stepSize',
+ label: '姝ュ簭闀垮害',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.stepSize ?? '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getTaskPathTemplateMergeStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || row.createBy$ || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || row.createTime$ || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || row.updateBy$ || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
+
diff --git a/rsf-design/src/views/basic-info/task-path-template-node/index.vue b/rsf-design/src/views/basic-info/task-path-template-node/index.vue
new file mode 100644
index 0000000..be5aefa
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-node/index.vue
@@ -0,0 +1,362 @@
+<template>
+ <div class="task-path-template-node-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鑺傜偣</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <TaskPathTemplateNodeDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :task-path-template-node-data="currentTaskPathTemplateNodeData"
+ @submit="handleDialogSubmit"
+ />
+
+ <TaskPathTemplateNodeDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchDeleteTaskPathTemplateNode,
+ fetchGetTaskPathTemplateNodeDetail,
+ fetchSaveTaskPathTemplateNode,
+ fetchTaskPathTemplateNodePage,
+ fetchUpdateTaskPathTemplateNode
+ } from '@/api/task-path-template-node'
+ import TaskPathTemplateNodeDialog from './modules/task-path-template-node-dialog.vue'
+ import TaskPathTemplateNodeDetailDrawer from './modules/task-path-template-node-detail-drawer.vue'
+ import { createTaskPathTemplateNodeTableColumns } from './taskPathTemplateNodeTable.columns'
+ import {
+ buildTaskPathTemplateNodeDialogModel,
+ buildTaskPathTemplateNodePageQueryParams,
+ buildTaskPathTemplateNodeSearchParams,
+ createTaskPathTemplateNodeFormState,
+ createTaskPathTemplateNodeSearchState,
+ getTaskPathTemplateNodePaginationKey,
+ getTaskPathTemplateNodeBooleanOptions,
+ getTaskPathTemplateNodeTypeOptions,
+ normalizeTaskPathTemplateNodeListRow
+ } from './taskPathTemplateNodePage.helpers'
+
+ defineOptions({ name: 'TaskPathTemplateNode' })
+
+ const { hasAuth } = useAuth()
+
+ const searchForm = ref(createTaskPathTemplateNodeSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユā鏉跨紪鐮�/鑺傜偣缂栫爜/鑺傜偣鍚嶇О'
+ }
+ },
+ {
+ label: '妯℃澘ID',
+ key: 'templateId',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユā鏉縄D'
+ }
+ },
+ {
+ label: '妯℃澘缂栫爜',
+ key: 'templateCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユā鏉跨紪鐮�'
+ }
+ },
+ {
+ label: '鑺傜偣椤哄簭',
+ key: 'nodeOrder',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ妭鐐归『搴�'
+ }
+ },
+ {
+ label: '鑺傜偣缂栫爜',
+ key: 'nodeCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ妭鐐圭紪鐮�'
+ }
+ },
+ {
+ label: '鑺傜偣鍚嶇О',
+ key: 'nodeName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ妭鐐瑰悕绉�'
+ }
+ },
+ {
+ label: '鑺傜偣绫诲瀷',
+ key: 'nodeType',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getTaskPathTemplateNodeTypeOptions()
+ }
+ },
+ {
+ label: '绯荤粺缂栫爜',
+ key: 'systemCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ郴缁熺紪鐮�'
+ }
+ },
+ {
+ label: '绯荤粺鍚嶇О',
+ key: 'systemName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ郴缁熷悕绉�'
+ }
+ },
+ {
+ label: '蹇呴』鑺傜偣',
+ key: 'mandatory',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getTaskPathTemplateNodeBooleanOptions()
+ }
+ },
+ {
+ label: '鍙苟琛�',
+ key: 'parallelExecutable',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getTaskPathTemplateNodeBooleanOptions()
+ }
+ },
+ {
+ label: '瓒呮椂(鍒�)',
+ key: 'timeoutMinutes',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ秴鏃舵椂闂�'
+ }
+ },
+ {
+ label: '鍓嶇疆鏉′欢',
+ key: 'preCondition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ墠缃潯浠�'
+ }
+ },
+ {
+ label: '鍚庣疆鏉′欢',
+ key: 'postCondition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悗缃潯浠�'
+ }
+ },
+ {
+ label: '涓嬩竴鑺傜偣瑙勫垯',
+ key: 'nextNodeRules',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ笅涓�鑺傜偣瑙勫垯'
+ }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨寮�濮嬫椂闂�'
+ }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨缁撴潫鏃堕棿'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetTaskPathTemplateNodeDetail(row.id), {}, {
+ timeoutMessage: '浠诲姟璺緞妯℃澘鑺傜偣璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeTaskPathTemplateNodeListRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇浠诲姟璺緞妯℃澘鑺傜偣璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetTaskPathTemplateNodeDetail(row.id), {}, {
+ timeoutMessage: '浠诲姟璺緞妯℃澘鑺傜偣璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇浠诲姟璺緞妯℃澘鑺傜偣璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchTaskPathTemplateNodePage,
+ apiParams: buildTaskPathTemplateNodePageQueryParams(searchForm.value),
+ paginationKey: getTaskPathTemplateNodePaginationKey(),
+ columnsFactory: () =>
+ createTaskPathTemplateNodeTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeTaskPathTemplateNodeListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentTaskPathTemplateNodeData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => createTaskPathTemplateNodeFormState(),
+ buildEditModel: (record) => buildTaskPathTemplateNodeDialogModel(record),
+ buildSavePayload: (formData) => buildTaskPathTemplateNodeDialogModel(formData),
+ saveRequest: fetchSaveTaskPathTemplateNode,
+ updateRequest: fetchUpdateTaskPathTemplateNode,
+ deleteRequest: fetchDeleteTaskPathTemplateNode,
+ entityName: '浠诲姟璺緞妯℃澘鑺傜偣',
+ resolveRecordLabel: (record) => record?.nodeName || record?.nodeCode || record?.templateCode || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTaskPathTemplateNodeSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTaskPathTemplateNodeSearchState())
+ resetSearchParams()
+ }
+
+ onMounted(() => {
+ getData()
+ })
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template-node/modules/task-path-template-node-detail-drawer.vue b/rsf-design/src/views/basic-info/task-path-template-node/modules/task-path-template-node-detail-drawer.vue
new file mode 100644
index 0000000..416de4b
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-node/modules/task-path-template-node-detail-drawer.vue
@@ -0,0 +1,88 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟璺緞妯℃澘鑺傜偣璇︽儏"
+ size="980px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="妯℃澘ID">{{ detail.templateId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="妯℃澘缂栫爜">{{ detail.templateCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣椤哄簭">{{ detail.nodeOrder ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣缂栫爜">{{ detail.nodeCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣鍚嶇О">{{ detail.nodeName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣绫诲瀷">
+ <ElTag :type="detail.nodeTypeType || 'info'" effect="light">
+ {{ detail.nodeTypeText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="绯荤粺缂栫爜">{{ detail.systemCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绯荤粺鍚嶇О">{{ detail.systemName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="蹇呴』鑺傜偣">
+ <ElTag :type="detail.mandatoryType || 'info'" effect="light">
+ {{ detail.mandatoryText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙苟琛�">
+ <ElTag :type="detail.parallelExecutableType || 'info'" effect="light">
+ {{ detail.parallelExecutableText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="瓒呮椂(鍒�)">{{ detail.timeoutMinutes ?? '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="鎵ц瑙勫垯" :column="2" border>
+ <ElDescriptionsItem label="鎵ц鍙傛暟" :span="2">
+ <pre class="m-0 whitespace-pre-wrap break-words text-[var(--art-text-secondary)]">{{ detail.executeParams || '--' }}</pre>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="缁撴灉缁撴瀯" :span="2">
+ <pre class="m-0 whitespace-pre-wrap break-words text-[var(--art-text-secondary)]">{{ detail.resultSchema || '--' }}</pre>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍓嶇疆鏉′欢" :span="2">
+ <pre class="m-0 whitespace-pre-wrap break-words text-[var(--art-text-secondary)]">{{ detail.preCondition || '--' }}</pre>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚庣疆鏉′欢" :span="2">
+ <pre class="m-0 whitespace-pre-wrap break-words text-[var(--art-text-secondary)]">{{ detail.postCondition || '--' }}</pre>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="涓嬩竴鑺傜偣瑙勫垯" :span="2">
+ <pre class="m-0 whitespace-pre-wrap break-words text-[var(--art-text-secondary)]">{{ detail.nextNodeRules || '--' }}</pre>
+ </ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || detail.createBy || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || detail.createTime || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || detail.updateBy || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || detail.updateTime || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template-node/modules/task-path-template-node-dialog.vue b/rsf-design/src/views/basic-info/task-path-template-node/modules/task-path-template-node-dialog.vue
new file mode 100644
index 0000000..f10842f
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-node/modules/task-path-template-node-dialog.vue
@@ -0,0 +1,283 @@
+<template>
+ <ElDialog
+ :model-value="visible"
+ :title="dialogTitle"
+ width="980px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="120px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildTaskPathTemplateNodeDialogModel,
+ createTaskPathTemplateNodeFormState,
+ getTaskPathTemplateNodeBooleanOptions,
+ getTaskPathTemplateNodeTypeOptions
+ } from '../taskPathTemplateNodePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ taskPathTemplateNodeData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createTaskPathTemplateNodeFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫浠诲姟璺緞妯℃澘鑺傜偣' : '鏂板浠诲姟璺緞妯℃澘鑺傜偣'))
+
+ const rules = computed(() => ({
+ templateId: [{ required: true, message: '璇疯緭鍏ユā鏉縄D', trigger: 'blur' }],
+ templateCode: [{ required: true, message: '璇疯緭鍏ユā鏉跨紪鐮�', trigger: 'blur' }],
+ nodeOrder: [{ required: true, message: '璇疯緭鍏ヨ妭鐐归『搴�', trigger: 'blur' }],
+ nodeCode: [{ required: true, message: '璇疯緭鍏ヨ妭鐐圭紪鐮�', trigger: 'blur' }],
+ nodeName: [{ required: true, message: '璇疯緭鍏ヨ妭鐐瑰悕绉�', trigger: 'blur' }],
+ nodeType: [{ required: true, message: '璇烽�夋嫨鑺傜偣绫诲瀷', trigger: 'change' }],
+ systemCode: [{ required: true, message: '璇疯緭鍏ョ郴缁熺紪鐮�', trigger: 'blur' }],
+ mandatory: [{ required: true, message: '璇烽�夋嫨鏄惁蹇呴』鑺傜偣', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '妯℃澘ID',
+ key: 'templateId',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユā鏉縄D'
+ }
+ },
+ {
+ label: '妯℃澘缂栫爜',
+ key: 'templateCode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユā鏉跨紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鑺傜偣椤哄簭',
+ key: 'nodeOrder',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ妭鐐归『搴�'
+ }
+ },
+ {
+ label: '鑺傜偣缂栫爜',
+ key: 'nodeCode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヨ妭鐐圭紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鑺傜偣鍚嶇О',
+ key: 'nodeName',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヨ妭鐐瑰悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '鑺傜偣绫诲瀷',
+ key: 'nodeType',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鑺傜偣绫诲瀷',
+ clearable: true,
+ options: getTaskPathTemplateNodeTypeOptions()
+ }
+ },
+ {
+ label: '绯荤粺缂栫爜',
+ key: 'systemCode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ郴缁熺紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '绯荤粺鍚嶇О',
+ key: 'systemName',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ郴缁熷悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '瓒呮椂(鍒�)',
+ key: 'timeoutMinutes',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ秴鏃舵椂闂�'
+ }
+ },
+ {
+ label: '蹇呴』鑺傜偣',
+ key: 'mandatory',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏄惁蹇呴』鑺傜偣',
+ clearable: true,
+ options: getTaskPathTemplateNodeBooleanOptions()
+ }
+ },
+ {
+ label: '鍙苟琛�',
+ key: 'parallelExecutable',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏄惁鍙苟琛�',
+ clearable: true,
+ options: getTaskPathTemplateNodeBooleanOptions()
+ }
+ },
+ {
+ label: '鎵ц鍙傛暟',
+ key: 'executeParams',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ヨ妭鐐规墽琛屽弬鏁版ā鏉�',
+ clearable: true
+ }
+ },
+ {
+ label: '缁撴灉缁撴瀯',
+ key: 'resultSchema',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ユ湡鏈涚粨鏋滄暟鎹牸寮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍓嶇疆鏉′欢',
+ key: 'preCondition',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ユ墽琛屽墠缃潯浠�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍚庣疆鏉′欢',
+ key: 'postCondition',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ユ墽琛屽悗缃潯浠�',
+ clearable: true
+ }
+ },
+ {
+ label: '涓嬩竴鑺傜偣瑙勫垯',
+ key: 'nextNodeRules',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ヤ笅涓�鑺傜偣璺敱瑙勫垯',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildTaskPathTemplateNodeDialogModel(props.taskPathTemplateNodeData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createTaskPathTemplateNodeFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.taskPathTemplateNodeData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template-node/taskPathTemplateNodePage.helpers.js b/rsf-design/src/views/basic-info/task-path-template-node/taskPathTemplateNodePage.helpers.js
new file mode 100644
index 0000000..92e944c
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-node/taskPathTemplateNodePage.helpers.js
@@ -0,0 +1,311 @@
+const NODE_TYPE_META = {
+ EXECUTE: { text: '鎵ц鑺傜偣', type: 'primary' },
+ CHECK: { text: '妫�鏌ョ偣', type: 'warning' },
+ DECISION: { text: '鍐崇瓥鐐�', type: 'success' }
+}
+
+const BOOLEAN_META = {
+ 1: { text: '鏄�', type: 'success', bool: true },
+ 0: { text: '鍚�', type: 'info', bool: false }
+}
+
+export const TASK_PATH_TEMPLATE_NODE_REPORT_TITLE = '浠诲姟璺緞妯℃澘鑺傜偣鎶ヨ〃'
+export const TASK_PATH_TEMPLATE_NODE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeMaybeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+function normalizeMaybeText(value) {
+ return normalizeText(value)
+}
+
+function normalizeMaybeBooleanNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+export function createTaskPathTemplateNodeSearchState() {
+ return {
+ condition: '',
+ templateId: void 0,
+ templateCode: '',
+ nodeOrder: void 0,
+ nodeCode: '',
+ nodeName: '',
+ nodeType: '',
+ systemCode: '',
+ systemName: '',
+ executeParams: '',
+ resultSchema: '',
+ timeoutMinutes: void 0,
+ mandatory: '',
+ parallelExecutable: '',
+ preCondition: '',
+ postCondition: '',
+ nextNodeRules: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createTaskPathTemplateNodeFormState() {
+ return {
+ id: void 0,
+ templateId: void 0,
+ templateCode: '',
+ nodeOrder: 1,
+ nodeCode: '',
+ nodeName: '',
+ nodeType: 'EXECUTE',
+ systemCode: '',
+ systemName: '',
+ executeParams: '',
+ resultSchema: '',
+ timeoutMinutes: void 0,
+ mandatory: 1,
+ parallelExecutable: 0,
+ preCondition: '',
+ postCondition: '',
+ nextNodeRules: ''
+ }
+}
+
+export function getTaskPathTemplateNodePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getTaskPathTemplateNodeTypeOptions() {
+ return [
+ { label: '鎵ц鑺傜偣', value: 'EXECUTE' },
+ { label: '妫�鏌ョ偣', value: 'CHECK' },
+ { label: '鍐崇瓥鐐�', value: 'DECISION' }
+ ]
+}
+
+export function getTaskPathTemplateNodeBooleanOptions() {
+ return [
+ { label: '鍚�', value: 0 },
+ { label: '鏄�', value: 1 }
+ ]
+}
+
+export function getTaskPathTemplateNodeTypeMeta(nodeType) {
+ const normalized = normalizeMaybeText(nodeType).toUpperCase()
+ if (normalized in NODE_TYPE_META) {
+ return NODE_TYPE_META[normalized]
+ }
+ return { text: normalized || '鏈煡', type: 'info' }
+}
+
+export function getTaskPathTemplateNodeBooleanMeta(value) {
+ if (value === true || Number(value) === 1) {
+ return BOOLEAN_META[1]
+ }
+ if (value === false || Number(value) === 0) {
+ return BOOLEAN_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildTaskPathTemplateNodeSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeMaybeText(params.condition),
+ templateId: normalizeMaybeNumber(params.templateId),
+ templateCode: normalizeMaybeText(params.templateCode),
+ nodeOrder: normalizeMaybeNumber(params.nodeOrder),
+ nodeCode: normalizeMaybeText(params.nodeCode),
+ nodeName: normalizeMaybeText(params.nodeName),
+ nodeType: normalizeMaybeText(params.nodeType),
+ systemCode: normalizeMaybeText(params.systemCode),
+ systemName: normalizeMaybeText(params.systemName),
+ executeParams: normalizeMaybeText(params.executeParams),
+ resultSchema: normalizeMaybeText(params.resultSchema),
+ timeoutMinutes: normalizeMaybeNumber(params.timeoutMinutes),
+ mandatory: normalizeMaybeNumber(params.mandatory),
+ parallelExecutable: normalizeMaybeNumber(params.parallelExecutable),
+ preCondition: normalizeMaybeText(params.preCondition),
+ postCondition: normalizeMaybeText(params.postCondition),
+ nextNodeRules: normalizeMaybeText(params.nextNodeRules),
+ timeStart: normalizeMaybeText(params.timeStart),
+ timeEnd: normalizeMaybeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildTaskPathTemplateNodePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTaskPathTemplateNodeSearchParams(params)
+ }
+}
+
+export function buildTaskPathTemplateNodeSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ ...(formData.templateId !== void 0 && formData.templateId !== null && formData.templateId !== ''
+ ? { templateId: Number(formData.templateId) }
+ : {}),
+ templateCode: normalizeMaybeText(formData.templateCode) || '',
+ ...(formData.nodeOrder !== void 0 && formData.nodeOrder !== null && formData.nodeOrder !== ''
+ ? { nodeOrder: Number(formData.nodeOrder) }
+ : { nodeOrder: 1 }),
+ nodeCode: normalizeMaybeText(formData.nodeCode) || '',
+ nodeName: normalizeMaybeText(formData.nodeName) || '',
+ nodeType: normalizeMaybeText(formData.nodeType) || 'EXECUTE',
+ systemCode: normalizeMaybeText(formData.systemCode) || '',
+ systemName: normalizeMaybeText(formData.systemName) || '',
+ executeParams: normalizeMaybeText(formData.executeParams) || '',
+ resultSchema: normalizeMaybeText(formData.resultSchema) || '',
+ ...(formData.timeoutMinutes !== void 0 && formData.timeoutMinutes !== null && formData.timeoutMinutes !== ''
+ ? { timeoutMinutes: Number(formData.timeoutMinutes) }
+ : {}),
+ ...(formData.mandatory !== void 0 && formData.mandatory !== null && formData.mandatory !== ''
+ ? { mandatory: Number(formData.mandatory) }
+ : { mandatory: 1 }),
+ ...(formData.parallelExecutable !== void 0 &&
+ formData.parallelExecutable !== null &&
+ formData.parallelExecutable !== ''
+ ? { parallelExecutable: Number(formData.parallelExecutable) }
+ : { parallelExecutable: 0 }),
+ preCondition: normalizeMaybeText(formData.preCondition) || '',
+ postCondition: normalizeMaybeText(formData.postCondition) || '',
+ nextNodeRules: normalizeMaybeText(formData.nextNodeRules) || ''
+ }
+}
+
+export function buildTaskPathTemplateNodeDialogModel(record = {}) {
+ return {
+ ...createTaskPathTemplateNodeFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ ...(record.templateId !== void 0 && record.templateId !== null && record.templateId !== ''
+ ? { templateId: Number(record.templateId) }
+ : {}),
+ templateCode: normalizeMaybeText(record.templateCode || ''),
+ nodeOrder:
+ record.nodeOrder !== void 0 && record.nodeOrder !== null && record.nodeOrder !== ''
+ ? Number(record.nodeOrder)
+ : 1,
+ nodeCode: normalizeMaybeText(record.nodeCode || ''),
+ nodeName: normalizeMaybeText(record.nodeName || ''),
+ nodeType: normalizeMaybeText(record.nodeType || '') || 'EXECUTE',
+ systemCode: normalizeMaybeText(record.systemCode || ''),
+ systemName: normalizeMaybeText(record.systemName || ''),
+ executeParams: normalizeMaybeText(record.executeParams || ''),
+ resultSchema: normalizeMaybeText(record.resultSchema || ''),
+ timeoutMinutes: normalizeMaybeNumber(record.timeoutMinutes, void 0),
+ mandatory:
+ record.mandatory !== void 0 && record.mandatory !== null && record.mandatory !== ''
+ ? Number(record.mandatory)
+ : 1,
+ parallelExecutable:
+ record.parallelExecutable !== void 0 && record.parallelExecutable !== null && record.parallelExecutable !== ''
+ ? Number(record.parallelExecutable)
+ : 0,
+ preCondition: normalizeMaybeText(record.preCondition || ''),
+ postCondition: normalizeMaybeText(record.postCondition || ''),
+ nextNodeRules: normalizeMaybeText(record.nextNodeRules || '')
+ }
+}
+
+export function normalizeTaskPathTemplateNodeDetailRecord(record = {}) {
+ const nodeTypeMeta = getTaskPathTemplateNodeTypeMeta(record.nodeType)
+ const mandatoryMeta = getTaskPathTemplateNodeBooleanMeta(record.mandatory)
+ const parallelMeta = getTaskPathTemplateNodeBooleanMeta(record.parallelExecutable)
+
+ return {
+ ...record,
+ templateId: normalizeMaybeNumber(record.templateId, void 0),
+ templateCode: normalizeMaybeText(record.templateCode || ''),
+ nodeOrder: normalizeMaybeNumber(record.nodeOrder, void 0),
+ nodeCode: normalizeMaybeText(record.nodeCode || ''),
+ nodeName: normalizeMaybeText(record.nodeName || ''),
+ nodeType: normalizeMaybeText(record.nodeType || ''),
+ nodeTypeText: nodeTypeMeta.text,
+ nodeTypeType: nodeTypeMeta.type,
+ systemCode: normalizeMaybeText(record.systemCode || ''),
+ systemName: normalizeMaybeText(record.systemName || ''),
+ executeParams: normalizeMaybeText(record.executeParams || ''),
+ resultSchema: normalizeMaybeText(record.resultSchema || ''),
+ timeoutMinutes: normalizeMaybeNumber(record.timeoutMinutes, void 0),
+ mandatory: normalizeMaybeNumber(record.mandatory, void 0),
+ mandatoryText: mandatoryMeta.text,
+ mandatoryType: mandatoryMeta.type,
+ mandatoryBool: record.mandatoryBool !== void 0 ? Boolean(record.mandatoryBool) : mandatoryMeta.bool,
+ parallelExecutable: normalizeMaybeNumber(record.parallelExecutable, void 0),
+ parallelExecutableText: parallelMeta.text,
+ parallelExecutableType: parallelMeta.type,
+ parallelExecutableBool:
+ record.parallelExecutableBool !== void 0 ? Boolean(record.parallelExecutableBool) : parallelMeta.bool,
+ preCondition: normalizeMaybeText(record.preCondition || ''),
+ postCondition: normalizeMaybeText(record.postCondition || ''),
+ nextNodeRules: normalizeMaybeText(record.nextNodeRules || ''),
+ createByText: normalizeMaybeText(record.createBy$ || record.createByText || ''),
+ updateByText: normalizeMaybeText(record.updateBy$ || record.updateByText || ''),
+ createTimeText: normalizeMaybeText(record.createTime$ || record.createTime || ''),
+ updateTimeText: normalizeMaybeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeTaskPathTemplateNodeListRow(record = {}) {
+ return normalizeTaskPathTemplateNodeDetailRecord(record)
+}
+
+export function buildTaskPathTemplateNodePrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeTaskPathTemplateNodeListRow(record))
+}
+
+export function buildTaskPathTemplateNodeReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = TASK_PATH_TEMPLATE_NODE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: TASK_PATH_TEMPLATE_NODE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...TASK_PATH_TEMPLATE_NODE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/task-path-template-node/taskPathTemplateNodeTable.columns.js b/rsf-design/src/views/basic-info/task-path-template-node/taskPathTemplateNodeTable.columns.js
new file mode 100644
index 0000000..68e74b3
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template-node/taskPathTemplateNodeTable.columns.js
@@ -0,0 +1,145 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import {
+ getTaskPathTemplateNodeBooleanMeta,
+ getTaskPathTemplateNodeTypeMeta
+} from './taskPathTemplateNodePage.helpers'
+
+export function createTaskPathTemplateNodeTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'templateId',
+ label: '妯℃澘ID',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.templateId ?? '--'
+ },
+ {
+ prop: 'templateCode',
+ label: '妯℃澘缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.templateCode || '--'
+ },
+ {
+ prop: 'nodeOrder',
+ label: '鑺傜偣椤哄簭',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.nodeOrder ?? '--'
+ },
+ {
+ prop: 'nodeCode',
+ label: '鑺傜偣缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.nodeCode || '--'
+ },
+ {
+ prop: 'nodeName',
+ label: '鑺傜偣鍚嶇О',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.nodeName || '--'
+ },
+ {
+ prop: 'nodeTypeText',
+ label: '鑺傜偣绫诲瀷',
+ width: 120,
+ align: 'center',
+ formatter: (row) => {
+ const typeMeta = getTaskPathTemplateNodeTypeMeta(row.nodeType)
+ return h(ElTag, { type: typeMeta.type, effect: 'light' }, () => typeMeta.text)
+ }
+ },
+ {
+ prop: 'systemCode',
+ label: '绯荤粺缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.systemCode || '--'
+ },
+ {
+ prop: 'systemName',
+ label: '绯荤粺鍚嶇О',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.systemName || '--'
+ },
+ {
+ prop: 'mandatoryText',
+ label: '蹇呴』鑺傜偣',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getTaskPathTemplateNodeBooleanMeta(row.mandatory)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'parallelExecutableText',
+ label: '鍙苟琛�',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getTaskPathTemplateNodeBooleanMeta(row.parallelExecutable)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'timeoutMinutes',
+ label: '瓒呮椂(鍒�)',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.timeoutMinutes ?? '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/task-path-template/index.vue b/rsf-design/src/views/basic-info/task-path-template/index.vue
new file mode 100644
index 0000000..88007c5
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template/index.vue
@@ -0,0 +1,428 @@
+<template>
+ <div class="task-path-template-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板妯℃澘</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <TaskPathTemplateDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :task-path-template-data="currentTaskPathTemplateData"
+ @submit="handleDialogSubmit"
+ />
+
+ <TaskPathTemplateDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+
+ <TaskPathTemplateFlowDrawer
+ v-model:visible="flowDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ buildTaskPathTemplatePageQueryParams,
+ buildTaskPathTemplatePrintRows,
+ buildTaskPathTemplateReportMeta,
+ buildTaskPathTemplateSavePayload,
+ buildTaskPathTemplateSearchParams,
+ createTaskPathTemplateSearchState,
+ getTaskPathTemplatePaginationKey,
+ normalizeTaskPathTemplateListRow,
+ TASK_PATH_TEMPLATE_REPORT_STYLE,
+ TASK_PATH_TEMPLATE_REPORT_TITLE
+ } from './taskPathTemplatePage.helpers'
+ import {
+ fetchDeleteTaskPathTemplate,
+ fetchExportTaskPathTemplateReport,
+ fetchGetTaskPathTemplateDetail,
+ fetchGetTaskPathTemplateMany,
+ fetchSaveTaskPathTemplate,
+ fetchTaskPathTemplatePage,
+ fetchUpdateTaskPathTemplate
+ } from '@/api/task-path-template'
+ import TaskPathTemplateDialog from './modules/task-path-template-dialog.vue'
+ import TaskPathTemplateDetailDrawer from './modules/task-path-template-detail-drawer.vue'
+ import TaskPathTemplateFlowDrawer from './modules/task-path-template-flow-drawer.vue'
+ import { createTaskPathTemplateTableColumns } from './taskPathTemplateTable.columns'
+
+ defineOptions({ name: 'TaskPathTemplate' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createTaskPathTemplateSearchState())
+ const detailDrawerVisible = ref(false)
+ const flowDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = TASK_PATH_TEMPLATE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildTaskPathTemplateSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユā鏉跨紪鐮�/鍚嶇О/鏉′欢鎻忚堪'
+ }
+ },
+ {
+ label: '妯℃澘缂栫爜',
+ key: 'templateCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユā鏉跨紪鐮�'
+ }
+ },
+ {
+ label: '妯℃澘鍚嶇О',
+ key: 'templateName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユā鏉垮悕绉�'
+ }
+ },
+ {
+ label: '璧风偣绫诲瀷',
+ key: 'sourceType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ捣鐐圭被鍨�'
+ }
+ },
+ {
+ label: '缁堢偣绫诲瀷',
+ key: 'targetType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ粓鐐圭被鍨�'
+ }
+ },
+ {
+ label: '鐗堟湰鍙�',
+ key: 'version',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ増鏈彿'
+ }
+ },
+ {
+ label: '褰撳墠鐗堟湰',
+ key: 'isCurrent',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '褰撳墠鐗堟湰', value: 1 },
+ { label: '鍘嗗彶鐗堟湰', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '浼樺厛绾�',
+ key: 'priority',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ紭鍏堢骇'
+ }
+ },
+ {
+ label: '瓒呮椂(鍒�)',
+ key: 'timeoutMinutes',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ秴鏃舵椂闂�'
+ }
+ },
+ {
+ label: '姝ュ簭闀垮害',
+ key: 'stepSize',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ搴忛暱搴�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鍚敤', value: 1 },
+ { label: '绂佺敤', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '鐢熸晥鏃堕棿',
+ key: 'effectiveTime',
+ type: 'datetime',
+ props: {
+ type: 'datetime',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨鐢熸晥鏃堕棿'
+ }
+ },
+ {
+ label: '澶辨晥鏃堕棿',
+ key: 'expireTime',
+ type: 'datetime',
+ props: {
+ type: 'datetime',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨澶辨晥鏃堕棿'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ flowDrawerVisible.value = false
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetTaskPathTemplateDetail(row.id), {}, {
+ timeoutMessage: '浠诲姟璺緞妯℃澘璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeTaskPathTemplateListRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇浠诲姟璺緞妯℃澘璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openFlow(row) {
+ detailDrawerVisible.value = false
+ flowDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetTaskPathTemplateDetail(row.id), {}, {
+ timeoutMessage: '娴佺▼鍥炬暟鎹姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ detailData.value = normalizeTaskPathTemplateListRow(detail)
+ } catch (error) {
+ flowDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇娴佺▼鍥炬暟鎹け璐�')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetTaskPathTemplateDetail(row.id), {}, {
+ timeoutMessage: '浠诲姟璺緞妯℃澘璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇浠诲姟璺緞妯℃澘璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchTaskPathTemplatePage,
+ apiParams: buildTaskPathTemplatePageQueryParams(searchForm.value),
+ paginationKey: getTaskPathTemplatePaginationKey(),
+ columnsFactory: () =>
+ createTaskPathTemplateTableColumns({
+ handleView: openDetail,
+ handleFlow: openFlow,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeTaskPathTemplateListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentTaskPathTemplateData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildTaskPathTemplateSavePayload({}),
+ buildEditModel: (record) => buildTaskPathTemplateSavePayload(record),
+ buildSavePayload: (formData) => buildTaskPathTemplateSavePayload(formData),
+ saveRequest: fetchSaveTaskPathTemplate,
+ updateRequest: fetchUpdateTaskPathTemplate,
+ deleteRequest: fetchDeleteTaskPathTemplate,
+ entityName: '浠诲姟璺緞妯℃澘',
+ resolveRecordLabel: (record) => record?.templateCode || record?.templateName || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...TASK_PATH_TEMPLATE_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetTaskPathTemplateMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchTaskPathTemplatePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'task-path-template.xlsx',
+ requestExport: (payload) =>
+ fetchExportTaskPathTemplateReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTaskPathTemplatePrintRows(records),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildTaskPathTemplateReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: TASK_PATH_TEMPLATE_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTaskPathTemplateSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTaskPathTemplateSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-detail-drawer.vue b/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-detail-drawer.vue
new file mode 100644
index 0000000..ce9f174
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-detail-drawer.vue
@@ -0,0 +1,76 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟璺緞妯℃澘璇︽儏"
+ size="980px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="妯℃澘缂栫爜">{{ detail.templateCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="妯℃澘鍚嶇О">{{ detail.templateName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璧风偣绫诲瀷">{{ detail.sourceType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁堢偣绫诲瀷">{{ detail.targetType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗堟湰鍙�">{{ detail.version ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="褰撳墠鐗堟湰">
+ <ElTag :type="detail.isCurrentType || 'info'" effect="light">
+ {{ detail.isCurrentText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鐢熸晥鏃堕棿">{{ detail.effectiveTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶辨晥鏃堕棿">{{ detail.expireTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浼樺厛绾�">{{ detail.priority ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瓒呮椂(鍒�)">{{ detail.timeoutMinutes ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ュ簭闀垮害">{{ detail.stepSize ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�澶ч噸璇曟鏁�">{{ detail.maxRetryTimes ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲嶈瘯闂撮殧(绉�)">{{ detail.retryIntervalSeconds ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉′欢鎻忚堪" :span="2">
+ {{ detail.conditionDesc || '--' }}
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉′欢琛ㄨ揪寮�" :span="2">
+ <pre class="m-0 whitespace-pre-wrap break-words text-[var(--art-text-secondary)]">{{ detail.conditionExpression || '--' }}</pre>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.remark || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-dialog.vue b/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-dialog.vue
new file mode 100644
index 0000000..4870ac3
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-dialog.vue
@@ -0,0 +1,294 @@
+<template>
+ <ElDialog
+ :model-value="visible"
+ :title="dialogTitle"
+ width="960px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="120px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildTaskPathTemplateDialogModel,
+ createTaskPathTemplateFormState,
+ getTaskPathTemplateCurrentOptions,
+ getTaskPathTemplateStatusOptions
+ } from '../taskPathTemplatePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ taskPathTemplateData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createTaskPathTemplateFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫浠诲姟璺緞妯℃澘' : '鏂板浠诲姟璺緞妯℃澘'))
+
+ const rules = computed(() => ({
+ templateCode: [{ required: true, message: '璇疯緭鍏ユā鏉跨紪鐮�', trigger: 'blur' }],
+ templateName: [{ required: true, message: '璇疯緭鍏ユā鏉垮悕绉�', trigger: 'blur' }],
+ sourceType: [{ required: true, message: '璇疯緭鍏ヨ捣鐐圭被鍨�', trigger: 'blur' }],
+ targetType: [{ required: true, message: '璇疯緭鍏ョ粓鐐圭被鍨�', trigger: 'blur' }],
+ version: [{ required: true, message: '璇疯緭鍏ョ増鏈彿', trigger: 'blur' }],
+ isCurrent: [{ required: true, message: '璇烽�夋嫨鏄惁褰撳墠鐗堟湰', trigger: 'change' }],
+ effectiveTime: [{ required: true, message: '璇烽�夋嫨鐢熸晥鏃堕棿', trigger: 'change' }],
+ priority: [{ required: true, message: '璇疯緭鍏ヤ紭鍏堢骇', trigger: 'blur' }],
+ status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '妯℃澘缂栫爜',
+ key: 'templateCode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユā鏉跨紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '妯℃澘鍚嶇О',
+ key: 'templateName',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユā鏉垮悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '璧风偣绫诲瀷',
+ key: 'sourceType',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヨ捣鐐圭被鍨�',
+ clearable: true
+ }
+ },
+ {
+ label: '缁堢偣绫诲瀷',
+ key: 'targetType',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ粓鐐圭被鍨�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐗堟湰鍙�',
+ key: 'version',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ増鏈彿'
+ }
+ },
+ {
+ label: '鏄惁褰撳墠鐗堟湰',
+ key: 'isCurrent',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鏄惁褰撳墠鐗堟湰',
+ clearable: true,
+ options: getTaskPathTemplateCurrentOptions()
+ }
+ },
+ {
+ label: '鐢熸晥鏃堕棿',
+ key: 'effectiveTime',
+ type: 'datetime',
+ props: {
+ type: 'datetime',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨鐢熸晥鏃堕棿'
+ }
+ },
+ {
+ label: '澶辨晥鏃堕棿',
+ key: 'expireTime',
+ type: 'datetime',
+ props: {
+ type: 'datetime',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨澶辨晥鏃堕棿'
+ }
+ },
+ {
+ label: '浼樺厛绾�',
+ key: 'priority',
+ type: 'number',
+ props: {
+ min: 1,
+ max: 99,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ紭鍏堢骇'
+ }
+ },
+ {
+ label: '瓒呮椂(鍒�)',
+ key: 'timeoutMinutes',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ暣浣撹秴鏃舵椂闂�'
+ }
+ },
+ {
+ label: '姝ュ簭闀垮害',
+ key: 'stepSize',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ搴忛暱搴�'
+ }
+ },
+ {
+ label: '鏈�澶ч噸璇曟鏁�',
+ key: 'maxRetryTimes',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ渶澶ч噸璇曟鏁�'
+ }
+ },
+ {
+ label: '閲嶈瘯闂撮殧(绉�)',
+ key: 'retryIntervalSeconds',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ラ噸璇曢棿闅�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getTaskPathTemplateStatusOptions()
+ }
+ },
+ {
+ label: '鏉′欢琛ㄨ揪寮�',
+ key: 'conditionExpression',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 4,
+ placeholder: '璇疯緭鍏ユ潯浠惰〃杈惧紡 JSON',
+ clearable: true
+ }
+ },
+ {
+ label: '鏉′欢鎻忚堪',
+ key: 'conditionDesc',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ユ潯浠舵弿杩�',
+ clearable: true
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'remark',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildTaskPathTemplateDialogModel(props.taskPathTemplateData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createTaskPathTemplateFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.taskPathTemplateData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue b/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue
new file mode 100644
index 0000000..c0f68ba
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template/modules/task-path-template-flow-drawer.vue
@@ -0,0 +1,78 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="娴佺▼鍥炬煡鐪�"
+ size="900px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="10" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElCard shadow="never" class="art-table-card">
+ <template #header>
+ <div class="flex items-center justify-between gap-3">
+ <div>
+ <h3 class="m-0 text-base font-semibold">妯℃澘娴佺▼蹇収</h3>
+ <p class="m-0 text-sm text-[var(--art-text-secondary)]">
+ 杩欓噷灞曠ず鐨勬槸鍚庣妯℃澘瀛楁缁勫悎鍑虹殑鐪熷疄娴佺▼淇℃伅锛屼笉鍋氶澶栧亣鏁版嵁鎺ㄦ紨銆�
+ </p>
+ </div>
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </div>
+ </template>
+
+ <div class="grid gap-3 md:grid-cols-4">
+ <div
+ v-for="item in flowSnapshot"
+ :key="item.key"
+ class="rounded-lg border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4"
+ >
+ <div class="text-sm text-[var(--art-text-secondary)]">{{ item.title }}</div>
+ <div class="mt-2 text-base font-semibold text-[var(--art-text-primary)]">
+ {{ item.value }}
+ </div>
+ </div>
+ </div>
+ </ElCard>
+
+ <ElDescriptions title="娴佺▼渚濇嵁" :column="2" border>
+ <ElDescriptionsItem label="妯℃澘缂栫爜">{{ detail.templateCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="妯℃澘鍚嶇О">{{ detail.templateName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璧风偣绫诲瀷">{{ detail.sourceType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁堢偣绫诲瀷">{{ detail.targetType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ュ簭闀垮害">{{ detail.stepSize ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浼樺厛绾�">{{ detail.priority ?? '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import { buildTaskPathTemplateFlowSnapshot } from '../taskPathTemplatePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ const flowSnapshot = computed(() => buildTaskPathTemplateFlowSnapshot(props.detail))
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/task-path-template/taskPathTemplatePage.helpers.js b/rsf-design/src/views/basic-info/task-path-template/taskPathTemplatePage.helpers.js
new file mode 100644
index 0000000..60ad90c
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template/taskPathTemplatePage.helpers.js
@@ -0,0 +1,282 @@
+const STATUS_META = {
+ 1: { text: '鍚敤', type: 'success', bool: true },
+ 0: { text: '绂佺敤', type: 'danger', bool: false }
+}
+
+const CURRENT_META = {
+ 1: { text: '褰撳墠鐗堟湰', type: 'success', bool: true },
+ 0: { text: '鍘嗗彶鐗堟湰', type: 'info', bool: false }
+}
+
+export const TASK_PATH_TEMPLATE_REPORT_TITLE = '浠诲姟璺緞妯℃澘鎶ヨ〃'
+export const TASK_PATH_TEMPLATE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+export function createTaskPathTemplateSearchState() {
+ return {
+ condition: '',
+ templateCode: '',
+ templateName: '',
+ sourceType: '',
+ targetType: '',
+ conditionExpression: '',
+ conditionDesc: '',
+ version: '',
+ isCurrent: '',
+ effectiveTime: '',
+ expireTime: '',
+ priority: '',
+ timeoutMinutes: '',
+ stepSize: '',
+ maxRetryTimes: '',
+ retryIntervalSeconds: '',
+ status: '',
+ remark: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createTaskPathTemplateFormState() {
+ return {
+ id: void 0,
+ templateCode: '',
+ templateName: '',
+ sourceType: '',
+ targetType: '',
+ conditionExpression: '',
+ conditionDesc: '',
+ version: 1,
+ isCurrent: 1,
+ effectiveTime: '',
+ expireTime: '',
+ priority: 1,
+ timeoutMinutes: '',
+ stepSize: '',
+ maxRetryTimes: 3,
+ retryIntervalSeconds: 60,
+ status: 1,
+ remark: ''
+ }
+}
+
+export function getTaskPathTemplateStatusOptions() {
+ return [
+ { label: '鍚敤', value: 1 },
+ { label: '绂佺敤', value: 0 }
+ ]
+}
+
+export function getTaskPathTemplateCurrentOptions() {
+ return [
+ { label: '褰撳墠鐗堟湰', value: 1 },
+ { label: '鍘嗗彶鐗堟湰', value: 0 }
+ ]
+}
+
+export function getTaskPathTemplatePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getTaskPathTemplateStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function getTaskPathTemplateCurrentMeta(isCurrent) {
+ if (isCurrent === true || Number(isCurrent) === 1) {
+ return CURRENT_META[1]
+ }
+ if (isCurrent === false || Number(isCurrent) === 0) {
+ return CURRENT_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildTaskPathTemplateSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ templateCode: normalizeText(params.templateCode),
+ templateName: normalizeText(params.templateName),
+ sourceType: normalizeText(params.sourceType),
+ targetType: normalizeText(params.targetType),
+ conditionExpression: normalizeText(params.conditionExpression),
+ conditionDesc: normalizeText(params.conditionDesc),
+ version: normalizeNumber(params.version),
+ isCurrent: normalizeNumber(params.isCurrent),
+ effectiveTime: normalizeText(params.effectiveTime),
+ expireTime: normalizeText(params.expireTime),
+ priority: normalizeNumber(params.priority),
+ timeoutMinutes: normalizeNumber(params.timeoutMinutes),
+ stepSize: normalizeNumber(params.stepSize),
+ maxRetryTimes: normalizeNumber(params.maxRetryTimes),
+ retryIntervalSeconds: normalizeNumber(params.retryIntervalSeconds),
+ status: normalizeNumber(params.status),
+ remark: normalizeText(params.remark),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildTaskPathTemplatePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTaskPathTemplateSearchParams(params)
+ }
+}
+
+export function buildTaskPathTemplateSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ templateCode: normalizeText(formData.templateCode),
+ templateName: normalizeText(formData.templateName),
+ sourceType: normalizeText(formData.sourceType),
+ targetType: normalizeText(formData.targetType),
+ conditionExpression: normalizeText(formData.conditionExpression),
+ conditionDesc: normalizeText(formData.conditionDesc),
+ version: normalizeNumber(formData.version, 1),
+ isCurrent: normalizeNumber(formData.isCurrent, 1),
+ effectiveTime: normalizeText(formData.effectiveTime),
+ expireTime: normalizeText(formData.expireTime),
+ priority: normalizeNumber(formData.priority, 1),
+ ...(formData.timeoutMinutes !== void 0 &&
+ formData.timeoutMinutes !== null &&
+ formData.timeoutMinutes !== ''
+ ? { timeoutMinutes: Number(formData.timeoutMinutes) }
+ : {}),
+ ...(formData.stepSize !== void 0 && formData.stepSize !== null && formData.stepSize !== ''
+ ? { stepSize: Number(formData.stepSize) }
+ : {}),
+ maxRetryTimes: normalizeNumber(formData.maxRetryTimes, 3),
+ retryIntervalSeconds: normalizeNumber(formData.retryIntervalSeconds, 60),
+ status: normalizeNumber(formData.status, 1),
+ remark: normalizeText(formData.remark)
+ }
+}
+
+export function buildTaskPathTemplateDialogModel(record = {}) {
+ return {
+ ...createTaskPathTemplateFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== ''
+ ? { id: Number(record.id) }
+ : {}),
+ templateCode: normalizeText(record.templateCode || ''),
+ templateName: normalizeText(record.templateName || ''),
+ sourceType: normalizeText(record.sourceType || ''),
+ targetType: normalizeText(record.targetType || ''),
+ conditionExpression: normalizeText(record.conditionExpression || ''),
+ conditionDesc: normalizeText(record.conditionDesc || ''),
+ version: normalizeNumber(record.version, 1),
+ isCurrent: normalizeNumber(record.isCurrent, 1),
+ effectiveTime: normalizeText(record.effectiveTime$ || record.effectiveTime || ''),
+ expireTime: normalizeText(record.expireTime$ || record.expireTime || ''),
+ priority: normalizeNumber(record.priority, 1),
+ timeoutMinutes: normalizeNumber(record.timeoutMinutes, ''),
+ stepSize: normalizeNumber(record.stepSize, ''),
+ maxRetryTimes: normalizeNumber(record.maxRetryTimes, 3),
+ retryIntervalSeconds: normalizeNumber(record.retryIntervalSeconds, 60),
+ status: normalizeNumber(record.status, 1),
+ remark: normalizeText(record.remark || '')
+ }
+}
+
+export function normalizeTaskPathTemplateDetailRecord(record = {}) {
+ const statusMeta = getTaskPathTemplateStatusMeta(record.statusBool ?? record.status)
+ const currentMeta = getTaskPathTemplateCurrentMeta(record.isCurrent)
+ return {
+ ...record,
+ templateCode: normalizeText(record.templateCode || ''),
+ templateName: normalizeText(record.templateName || ''),
+ sourceType: normalizeText(record.sourceType || ''),
+ targetType: normalizeText(record.targetType || ''),
+ conditionExpression: normalizeText(record.conditionExpression || ''),
+ conditionDesc: normalizeText(record.conditionDesc || ''),
+ remark: normalizeText(record.remark || ''),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ isCurrentText: currentMeta.text,
+ isCurrentType: currentMeta.type,
+ isCurrentBool: record.isCurrent !== void 0 ? Boolean(record.isCurrent) : currentMeta.bool,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ effectiveTimeText: normalizeText(record.effectiveTime$ || record.effectiveTime || ''),
+ expireTimeText: normalizeText(record.expireTime$ || record.expireTime || '')
+ }
+}
+
+export function normalizeTaskPathTemplateListRow(record = {}) {
+ return normalizeTaskPathTemplateDetailRecord(record)
+}
+
+export function buildTaskPathTemplatePrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeTaskPathTemplateListRow(record))
+}
+
+export function buildTaskPathTemplateReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = TASK_PATH_TEMPLATE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: TASK_PATH_TEMPLATE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...TASK_PATH_TEMPLATE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function buildTaskPathTemplateFlowSnapshot(record = {}) {
+ const sourceType = normalizeText(record.sourceType || '')
+ const targetType = normalizeText(record.targetType || '')
+ const stepSize = normalizeText(record.stepSize || '')
+ const templateCode = normalizeText(record.templateCode || '')
+ return [
+ { key: 'source', title: '璧风偣绫诲瀷', value: sourceType || '--' },
+ { key: 'step', title: '姝ュ簭闀垮害', value: stepSize || '--' },
+ { key: 'target', title: '缁堢偣绫诲瀷', value: targetType || '--' },
+ { key: 'code', title: '妯℃澘缂栫爜', value: templateCode || '--' }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/task-path-template/taskPathTemplateTable.columns.js b/rsf-design/src/views/basic-info/task-path-template/taskPathTemplateTable.columns.js
new file mode 100644
index 0000000..873ab26
--- /dev/null
+++ b/rsf-design/src/views/basic-info/task-path-template/taskPathTemplateTable.columns.js
@@ -0,0 +1,207 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import {
+ getTaskPathTemplateCurrentMeta,
+ getTaskPathTemplateStatusMeta
+} from './taskPathTemplatePage.helpers'
+
+export function createTaskPathTemplateTableColumns({
+ handleView,
+ handleFlow,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true
+} = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (handleFlow) {
+ operations.push({ key: 'flow', label: '娴佺▼鍥�', icon: 'ri:route-line' })
+ }
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'templateCode',
+ label: '妯℃澘缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.templateCode || '--'
+ },
+ {
+ prop: 'templateName',
+ label: '妯℃澘鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.templateName || '--'
+ },
+ {
+ prop: 'sourceType',
+ label: '璧风偣绫诲瀷',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.sourceType || '--'
+ },
+ {
+ prop: 'targetType',
+ label: '缁堢偣绫诲瀷',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.targetType || '--'
+ },
+ {
+ prop: 'conditionDesc',
+ label: '鏉′欢鎻忚堪',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.conditionDesc || '--'
+ },
+ {
+ prop: 'version',
+ label: '鐗堟湰鍙�',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.version ?? '--'
+ },
+ {
+ prop: 'isCurrent',
+ label: '褰撳墠鐗堟湰',
+ width: 110,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getTaskPathTemplateCurrentMeta(row.isCurrentBool ?? row.isCurrent)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'effectiveTimeText',
+ label: '鐢熸晥鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.effectiveTimeText || row.effectiveTime$ || '--'
+ },
+ {
+ prop: 'expireTimeText',
+ label: '澶辨晥鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.expireTimeText || row.expireTime$ || '--'
+ },
+ {
+ prop: 'priority',
+ label: '浼樺厛绾�',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.priority ?? '--'
+ },
+ {
+ prop: 'timeoutMinutes',
+ label: '瓒呮椂(鍒�)',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.timeoutMinutes ?? '--'
+ },
+ {
+ prop: 'stepSize',
+ label: '姝ュ簭闀垮害',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.stepSize ?? '--'
+ },
+ {
+ prop: 'maxRetryTimes',
+ label: '鏈�澶ч噸璇�',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.maxRetryTimes ?? '--'
+ },
+ {
+ prop: 'retryIntervalSeconds',
+ label: '閲嶈瘯闂撮殧(绉�)',
+ width: 120,
+ align: 'center',
+ formatter: (row) => row.retryIntervalSeconds ?? '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const meta = getTaskPathTemplateStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: meta.type, effect: 'light' }, () => meta.text)
+ }
+ },
+ {
+ prop: 'remark',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.remark || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || row.updateBy$ || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || row.createBy$ || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || row.createTime$ || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 190,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'flow') handleFlow?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/basic-info/warehouse/index.vue b/rsf-design/src/views/basic-info/warehouse/index.vue
new file mode 100644
index 0000000..b1532fd
--- /dev/null
+++ b/rsf-design/src/views/basic-info/warehouse/index.vue
@@ -0,0 +1,325 @@
+<template>
+ <div class="warehouse-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板浠撳簱</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <WarehouseDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :warehouse-data="currentWarehouseData"
+ @submit="handleDialogSubmit"
+ />
+
+ <WarehouseDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchDeleteWarehouse,
+ fetchExportWarehouseReport,
+ fetchGetWarehouseDetail,
+ fetchGetWarehouseMany,
+ fetchSaveWarehouse,
+ fetchUpdateWarehouse,
+ fetchWarehousePage
+ } from '@/api/warehouse'
+ import WarehouseDialog from './modules/warehouse-dialog.vue'
+ import WarehouseDetailDrawer from './modules/warehouse-detail-drawer.vue'
+ import { createWarehouseTableColumns } from './warehouseTable.columns'
+ import {
+ buildWarehouseDialogModel,
+ buildWarehousePageQueryParams,
+ buildWarehousePrintRows,
+ buildWarehouseReportMeta,
+ buildWarehouseSavePayload,
+ buildWarehouseSearchParams,
+ createWarehouseSearchState,
+ getWarehousePaginationKey,
+ normalizeWarehouseDetailRecord,
+ normalizeWarehouseListRow,
+ WAREHOUSE_REPORT_STYLE,
+ WAREHOUSE_REPORT_TITLE
+ } from './warehousePage.helpers'
+
+ defineOptions({ name: 'Warehouse' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createWarehouseSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = WAREHOUSE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildWarehouseSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ粨搴撳悕绉�/缂栫爜/宸ュ巶'
+ }
+ },
+ {
+ label: '浠撳簱鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ粨搴撳悕绉�'
+ }
+ },
+ {
+ label: '浠撳簱缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ粨搴撶紪鐮�'
+ }
+ },
+ {
+ label: '鎵�灞炲伐鍘�',
+ key: 'factory',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ墍灞炲伐鍘�'
+ }
+ },
+ {
+ label: '浠撳簱鍦板潃',
+ key: 'address',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ粨搴撳湴鍧�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetWarehouseDetail(row.id), {}, {
+ timeoutMessage: '浠撳簱璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeWarehouseDetailRecord(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇浠撳簱璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetWarehouseDetail(row.id), {}, {
+ timeoutMessage: '浠撳簱璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇浠撳簱璇︽儏澶辫触')
+ }
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchWarehousePage,
+ apiParams: buildWarehousePageQueryParams(searchForm.value),
+ paginationKey: getWarehousePaginationKey(),
+ columnsFactory: () =>
+ createWarehouseTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete')
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeWarehouseListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentWarehouseData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildWarehouseDialogModel(),
+ buildEditModel: (record) => buildWarehouseDialogModel(record),
+ buildSavePayload: (formData) => buildWarehouseSavePayload(formData),
+ saveRequest: fetchSaveWarehouse,
+ updateRequest: fetchUpdateWarehouse,
+ deleteRequest: fetchDeleteWarehouse,
+ entityName: '浠撳簱',
+ resolveRecordLabel: (record) => record?.name || record?.code || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...WAREHOUSE_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetWarehouseMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchWarehousePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'warehouse.xlsx',
+ requestExport: (payload) =>
+ fetchExportWarehouseReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildWarehousePrintRows(records),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildWarehouseReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || WAREHOUSE_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildWarehouseSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createWarehouseSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/warehouse/modules/warehouse-detail-drawer.vue b/rsf-design/src/views/basic-info/warehouse/modules/warehouse-detail-drawer.vue
new file mode 100644
index 0000000..6ddd5ac
--- /dev/null
+++ b/rsf-design/src/views/basic-info/warehouse/modules/warehouse-detail-drawer.vue
@@ -0,0 +1,57 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠撳簱璇︽儏"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="浠撳簱鍚嶇О">{{ detail.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱缂栫爜">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵�灞炲伐鍘�">{{ detail.factory || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱鍦板潃">{{ detail.address || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/basic-info/warehouse/modules/warehouse-dialog.vue b/rsf-design/src/views/basic-info/warehouse/modules/warehouse-dialog.vue
new file mode 100644
index 0000000..9d78bf3
--- /dev/null
+++ b/rsf-design/src/views/basic-info/warehouse/modules/warehouse-dialog.vue
@@ -0,0 +1,167 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="880px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildWarehouseDialogModel, createWarehouseFormState, getWarehouseStatusOptions } from '../warehousePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ warehouseData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createWarehouseFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫浠撳簱' : '鏂板浠撳簱'))
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ヤ粨搴撳悕绉�', trigger: 'blur' }],
+ code: [{ required: true, message: '璇疯緭鍏ヤ粨搴撶紪鐮�', trigger: 'blur' }],
+ factory: [{ required: true, message: '璇疯緭鍏ユ墍灞炲伐鍘�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '浠撳簱鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヤ粨搴撳悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '浠撳簱缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヤ粨搴撶紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鎵�灞炲伐鍘�',
+ key: 'factory',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ墍灞炲伐鍘�',
+ clearable: true
+ }
+ },
+ {
+ label: '浠撳簱鍦板潃',
+ key: 'address',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ヤ粨搴撳湴鍧�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getWarehouseStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildWarehouseDialogModel(props.warehouseData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createWarehouseFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.warehouseData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/basic-info/warehouse/warehousePage.helpers.js b/rsf-design/src/views/basic-info/warehouse/warehousePage.helpers.js
new file mode 100644
index 0000000..39e3bd1
--- /dev/null
+++ b/rsf-design/src/views/basic-info/warehouse/warehousePage.helpers.js
@@ -0,0 +1,182 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const WAREHOUSE_REPORT_TITLE = '浠撳簱鎶ヨ〃'
+export const WAREHOUSE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numberValue = Number(value)
+ return Number.isNaN(numberValue) ? fallback : numberValue
+}
+
+export function createWarehouseSearchState() {
+ return {
+ condition: '',
+ factory: '',
+ code: '',
+ name: '',
+ address: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createWarehouseFormState() {
+ return {
+ id: void 0,
+ name: '',
+ code: '',
+ factory: '',
+ address: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getWarehouseStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getWarehousePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getWarehouseStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildWarehouseSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ factory: normalizeText(params.factory),
+ code: normalizeText(params.code),
+ name: normalizeText(params.name),
+ address: normalizeText(params.address),
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? Number(params.status)
+ : void 0,
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildWarehousePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWarehouseSearchParams(params)
+ }
+}
+
+export function buildWarehouseSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== void 0 && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ name: normalizeText(formData.name),
+ code: normalizeText(formData.code),
+ factory: normalizeText(formData.factory),
+ address: normalizeText(formData.address),
+ status:
+ formData.status !== void 0 && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo)
+ }
+}
+
+export function buildWarehouseDialogModel(record = {}) {
+ return {
+ ...createWarehouseFormState(),
+ ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ name: normalizeText(record.name || ''),
+ code: normalizeText(record.code || ''),
+ factory: normalizeText(record.factory || ''),
+ address: normalizeText(record.address || ''),
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizeWarehouseDetailRecord(record = {}) {
+ const statusMeta = getWarehouseStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ name: normalizeText(record.name || ''),
+ code: normalizeText(record.code || ''),
+ factory: normalizeText(record.factory || ''),
+ address: normalizeText(record.address || ''),
+ memo: normalizeText(record.memo || ''),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record.createBy$ || record.createByText || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
+
+export function normalizeWarehouseListRow(record = {}) {
+ return normalizeWarehouseDetailRecord(record)
+}
+
+export function buildWarehousePrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeWarehouseListRow(record))
+}
+
+export function buildWarehouseReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = WAREHOUSE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: WAREHOUSE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...WAREHOUSE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/basic-info/warehouse/warehouseTable.columns.js b/rsf-design/src/views/basic-info/warehouse/warehouseTable.columns.js
new file mode 100644
index 0000000..573a293
--- /dev/null
+++ b/rsf-design/src/views/basic-info/warehouse/warehouseTable.columns.js
@@ -0,0 +1,118 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getWarehouseStatusMeta } from './warehousePage.helpers'
+
+export function createWarehouseTableColumns({ handleView, handleEdit, handleDelete, canEdit = true, canDelete = true } = {}) {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'name',
+ label: '浠撳簱鍚嶇О',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.name || '--'
+ },
+ {
+ prop: 'code',
+ label: '浠撳簱缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'factory',
+ label: '鎵�灞炲伐鍘�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.factory || '--'
+ },
+ {
+ prop: 'address',
+ label: '浠撳簱鍦板潃',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.address || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getWarehouseStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || row.updateBy$ || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || row.createBy$ || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || row.createTime$ || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/dashboard/console/consolePage.helpers.js b/rsf-design/src/views/dashboard/console/consolePage.helpers.js
new file mode 100644
index 0000000..8cd2b64
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/consolePage.helpers.js
@@ -0,0 +1,164 @@
+import { DEFAULT_REQUEST_GUARD_TIMEOUT, withRequestGuard } from '../../../utils/sys/requestGuard.js'
+
+const EMPTY_SUMMARY = {
+ pendingIn: 0,
+ pendingOut: 0,
+ completedIn: 0,
+ completedOut: 0,
+ taskQty: 0
+}
+const DASHBOARD_SECTION_TIMEOUT = DEFAULT_REQUEST_GUARD_TIMEOUT
+
+function buildDashboardDeadStockQuery() {
+ return {
+ current: 1,
+ pageSize: 10,
+ orderBy: 'create_time asc'
+ }
+}
+
+function buildDashboardTaskQuery() {
+ return {
+ current: 1,
+ pageSize: 100,
+ orderBy: 'sort desc'
+ }
+}
+
+function unwrapDashboardPayload(payload) {
+ let current = payload
+
+ while (
+ current &&
+ typeof current === 'object' &&
+ typeof current.code === 'number' &&
+ Object.prototype.hasOwnProperty.call(current, 'data')
+ ) {
+ current = current.data
+ }
+
+ return current || {}
+}
+
+function normalizeDashboardSummary(payload) {
+ const data = unwrapDashboardPayload(payload)
+ return {
+ pendingIn: normalizeNumber(data?.inAnf),
+ pendingOut: normalizeNumber(data?.outAnf),
+ completedIn: normalizeNumber(data?.taskIn),
+ completedOut: normalizeNumber(data?.taskOut),
+ taskQty: normalizeNumber(data?.taskQty)
+ }
+}
+
+function normalizeDashboardTrend(payload) {
+ const data = unwrapDashboardPayload(payload)
+ const items = Array.isArray(data?.trandItem) ? data.trandItem : []
+
+ return {
+ xAxisData: items.map((item) => formatTrendDate(item?.orderTime)),
+ series: [
+ {
+ name: '鍏ュ簱鏁伴噺',
+ data: items.map((item) => normalizeNumber(item?.inQty))
+ },
+ {
+ name: '鍑哄簱鏁伴噺',
+ data: items.map((item) => normalizeNumber(item?.outQty))
+ }
+ ],
+ maxQty: normalizeNumber(data?.maxQty)
+ }
+}
+
+function normalizeDashboardDeadStockList(payload) {
+ const records = Array.isArray(payload?.records) ? payload.records : []
+
+ return records.map((item) => ({
+ title: [item?.matnrCode, item?.maktx].filter(Boolean).join(' 路 ') || '-',
+ status: `鏁伴噺 ${formatAmount(item?.anfme)} 路 鍛嗘粸 ${item?.deadTime || '-'}`,
+ time: item?.locCode || '-',
+ icon: 'ri:archive-stack-line',
+ class: 'bg-amber-50 text-amber-600'
+ }))
+}
+
+function normalizeDashboardTaskList(payload) {
+ const records = Array.isArray(payload?.records) ? payload.records : []
+
+ return records.map((item) => ({
+ title: item?.taskCode || '-',
+ status: [item?.taskType$, item?.taskStatus$].filter(Boolean).join(' 路 ') || '寰呭鐞嗕换鍔�',
+ time: formatDateTime(item?.createTime),
+ icon: resolveTaskIcon(item?.taskStatus$),
+ class: resolveTaskClass(item?.taskStatus$)
+ }))
+}
+
+function normalizeDashboardLocUsage(payload) {
+ const data = unwrapDashboardPayload(payload)
+ const items = Array.isArray(data) ? data : []
+ return items.map((item) => ({
+ name: item?.name || '-',
+ value: normalizeNumber(item?.value)
+ }))
+}
+
+function withDashboardRequestGuard(task, fallbackValue, timeoutMs = DASHBOARD_SECTION_TIMEOUT) {
+ return withRequestGuard(task, fallbackValue, { timeoutMs })
+}
+
+function formatTrendDate(value) {
+ if (!value) return '--'
+ const text = String(value)
+ return text.length >= 10 ? text.slice(5, 10) : text
+}
+
+function formatDateTime(value) {
+ if (!value) return '--'
+ const text = String(value).replace('T', ' ')
+ if (text.length >= 16) {
+ return text.slice(5, 16)
+ }
+ return text
+}
+
+function formatAmount(value) {
+ const amount = Number(value)
+ if (!Number.isFinite(amount)) return '0'
+ if (Number.isInteger(amount)) return String(amount)
+ return amount.toFixed(2)
+}
+
+function normalizeNumber(value) {
+ const amount = Number(value)
+ return Number.isFinite(amount) ? amount : 0
+}
+
+function resolveTaskIcon(statusText) {
+ const text = String(statusText || '')
+ if (text.includes('瀹屾垚')) return 'ri:checkbox-circle-line'
+ if (text.includes('寮傚父') || text.includes('澶辫触')) return 'ri:error-warning-line'
+ return 'ri:task-line'
+}
+
+function resolveTaskClass(statusText) {
+ const text = String(statusText || '')
+ if (text.includes('瀹屾垚')) return 'bg-emerald-50 text-emerald-600'
+ if (text.includes('寮傚父') || text.includes('澶辫触')) return 'bg-rose-50 text-rose-600'
+ return 'bg-sky-50 text-sky-600'
+}
+
+export {
+ DASHBOARD_SECTION_TIMEOUT,
+ EMPTY_SUMMARY,
+ buildDashboardDeadStockQuery,
+ buildDashboardTaskQuery,
+ normalizeDashboardDeadStockList,
+ normalizeDashboardLocUsage,
+ normalizeDashboardSummary,
+ normalizeDashboardTaskList,
+ normalizeDashboardTrend,
+ unwrapDashboardPayload,
+ withDashboardRequestGuard
+}
diff --git a/rsf-design/src/views/dashboard/console/index.vue b/rsf-design/src/views/dashboard/console/index.vue
index 90691e1..1a2f229 100644
--- a/rsf-design/src/views/dashboard/console/index.vue
+++ b/rsf-design/src/views/dashboard/console/index.vue
@@ -1,145 +1,145 @@
<template>
- <div class="art-full-height flex flex-col gap-5">
- <section
- class="overflow-hidden rounded-3xl border border-white/10 bg-[linear-gradient(135deg,var(--art-main-bg-color),var(--art-card-bg-color))] p-6 shadow-[0_24px_80px_rgba(15,23,42,0.08)]"
- >
- <div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
- <div class="max-w-3xl">
- <div
- class="mb-3 inline-flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-600"
- >
- <span class="size-2 rounded-full bg-emerald-500"></span>
- RSF Phase 1 Landing
+ <div class="art-full-height flex flex-col gap-6">
+ <section class="grid gap-6 md:grid-cols-2 xl:grid-cols-4" v-loading="sectionLoading.summary">
+ <div
+ v-for="item in summaryCardItems"
+ :key="item.title"
+ class="art-card flex items-start justify-between rounded-3xl px-7 py-6"
+ >
+ <div class="min-w-0 pr-6">
+ <p class="text-sm font-medium text-g-700">{{ item.title }}</p>
+ <ArtCountTo class="mt-3 block text-[2.3rem] font-semibold leading-none text-g-900" :target="item.count" :duration="1400" />
+ <div class="mt-4 flex items-center gap-2 text-sm">
+ <span :class="item.metaTone">{{ item.metaLabel }}</span>
+ <span class="text-g-500">{{ item.metaValue }}</span>
</div>
- <h1
- class="m-0 text-3xl font-semibold tracking-tight text-[var(--art-gray-900)] md:text-4xl"
- >
- 杩愯楠ㄦ灦宸茬粡鍒囧埌 `rsf-design`
- </h1>
- <p class="mt-4 max-w-2xl text-sm leading-7 text-[var(--art-gray-600)] md:text-base">
- 褰撳墠鍏ュ彛宸茬粡鎺ュ叆鐪熷疄鍚庣鐧诲綍銆佸姩鎬佽彍鍗曞拰鏉冮檺閾捐矾銆傝繖涓椤靛彧灞曠ず宸茬粡鍙敤鐨� phase-1
- 鑳藉姏锛屼笉鍐嶄繚鐣欐ā鏉块噷鐨勭ず渚嬪浘琛ㄥ拰婕旂ず鏁版嵁銆�
- </p>
</div>
-
- <div class="rounded-2xl border border-white/10 bg-white/70 px-4 py-3 backdrop-blur-sm">
- <p class="text-xs uppercase tracking-[0.24em] text-[var(--art-gray-500)]"
- >Current Entry</p
- >
- <p class="mt-2 text-lg font-semibold text-[var(--art-gray-900)]">
- {{ currentUserName }}
- </p>
- <p class="mt-1 text-sm text-[var(--art-gray-600)]">
- {{ currentUserRoleText }} 路 {{ currentMenuLabel }}
- </p>
+ <div class="flex size-13 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass">
+ <ArtSvgIcon :icon="item.icon" class="text-2xl" :class="item.iconClass" />
</div>
</div>
</section>
- <section class="grid gap-4 md:grid-cols-3">
- <ArtStatsCard
- title="宸叉帴鍏ュ悗绔�"
- :count="backendSwitchCount"
- description="鐧诲綍銆佺敤鎴蜂俊鎭�佽彍鍗曞叏閮ㄦ潵鑷� rsf-server"
- icon="ri:server-line"
- />
- <ArtStatsCard
- title="鍔ㄦ�佽彍鍗�"
- :count="visibleMenuCount"
- description="浠呭彂甯� phase-1 鍏佽杩涘叆鐨勬柊鍏ュ彛"
- icon="ri:route-line"
- />
- <ArtStatsCard
- title="鏉冮檺閾捐矾"
- :count="permissionSignalCount"
- description="瑙掕壊涓庢潈闄愯妭鐐瑰凡浠庣湡瀹炵敤鎴锋暟鎹仮澶�"
- icon="ri:shield-check-line"
- />
+ <section class="grid gap-6 xl:grid-cols-[1.35fr_1fr]">
+ <div class="art-card h-115 overflow-hidden p-6 box-border">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>杩� 30 澶╁嚭鍏ュ簱瓒嬪娍</h4>
+ <p>鐪熷疄閾捐矾 <span class="text-success">宸叉帴閫�</span></p>
+ </div>
+ </div>
+ <div class="h-[calc(100%-4.5rem)]">
+ <ArtBarChart
+ height="22rem"
+ :loading="sectionLoading.trend"
+ :data="trendChartSeries"
+ :x-axis-data="trendChartXAxisData"
+ :show-axis-line="false"
+ :show-legend="true"
+ :show-split-line="true"
+ legend-position="top"
+ bar-width="38%"
+ />
+ </div>
+ </div>
+
+ <div class="art-card h-115 overflow-hidden p-6 box-border" v-loading="sectionLoading.locUsage">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>搴撲綅浣跨敤鍒嗗竷</h4>
+ <p>{{ usageLegendCount }} 涓淮搴�</p>
+ </div>
+ </div>
+ <div class="grid h-[calc(100%-4.5rem)] gap-6 lg:grid-cols-[1fr_0.95fr] lg:items-center">
+ <ArtRingChart
+ height="21rem"
+ :data="locUsageList"
+ center-text="搴撲綅鍗犳瘮"
+ :show-legend="false"
+ :show-label="false"
+ />
+
+ <div class="space-y-1">
+ <div
+ v-for="item in usageLegend"
+ :key="item.name"
+ class="flex items-center justify-between border-b border-[var(--art-border-color)] py-4 last:border-b-0"
+ >
+ <div class="flex items-center gap-3">
+ <span class="size-2.5 rounded-full" :style="{ backgroundColor: item.color }"></span>
+ <span class="text-sm text-[var(--art-gray-900)]">{{ item.name }}</span>
+ </div>
+ <span class="text-sm font-medium text-[var(--art-gray-700)]">{{ item.value }}%</span>
+ </div>
+
+ <ElEmpty v-if="!locUsageList.length" description="鏆傛棤搴撲綅浣跨敤鏁版嵁" :image-size="88" />
+ </div>
+ </div>
+ </div>
</section>
- <section class="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
- <ElCard
- class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]"
- shadow="never"
- >
- <template #header>
- <div class="flex items-center justify-between">
- <div>
- <h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">鐪熷疄杩愯鐘舵��</h2>
- <p class="mt-1 text-xs text-[var(--art-gray-500)]">
- 杩欎簺淇℃伅鏉ヨ嚜褰撳墠鐧诲綍鐢ㄦ埛鍜岃彍鍗� store
- </p>
- </div>
- <ElTag type="success" effect="light">Backend mode</ElTag>
- </div>
- </template>
-
- <div class="grid gap-4 md:grid-cols-2">
- <div class="rounded-2xl bg-[var(--art-gray-50)] p-4">
- <p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">User</p>
- <p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]">
- {{ currentUserName }}
- </p>
- <p class="mt-2 text-sm text-[var(--art-gray-600)]">
- {{ currentUserRoleText }}
- </p>
- <div class="mt-4 flex flex-wrap gap-2">
- <ElTag v-for="role in currentRoles" :key="role" type="info" effect="plain">
- {{ role }}
- </ElTag>
- <ElTag v-if="!currentRoles.length" type="info" effect="plain">No roles</ElTag>
- </div>
- </div>
-
- <div class="rounded-2xl bg-[var(--art-gray-50)] p-4">
- <p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">Permissions</p>
- <p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]">
- {{ currentAuthorities.length }} auth nodes
- </p>
- <p class="mt-2 text-sm text-[var(--art-gray-600)]">
- 鏉冮檺鑺傜偣鐩存帴鏉ヨ嚜褰撳墠鐢ㄦ埛鐨勭湡瀹� `authorities` 杞借嵎锛屼笉鍐嶄緷璧栨ā鏉挎紨绀烘�併��
- </p>
- <div class="mt-4 flex flex-wrap gap-2">
- <ElTag v-for="item in previewAuthorities" :key="item" type="warning" effect="plain">
- {{ item }}
- </ElTag>
- <ElTag v-if="!previewAuthorities.length" type="warning" effect="plain">
- No authorities
- </ElTag>
- </div>
+ <section class="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
+ <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.tasks">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>鎵ц涓换鍔�</h4>
+ <p>{{ taskSubtitle }}</p>
</div>
</div>
- </ElCard>
- <ElCard
- class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]"
- shadow="never"
- >
- <template #header>
- <div>
- <h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">鑿滃崟鎺ュ叆娓呭崟</h2>
- <p class="mt-1 text-xs text-[var(--art-gray-500)]">
- 褰撳墠鑿滃崟鏍戠敤浜庨獙璇� `rsf-design` 鏄惁鐪熸鎺ヤ綇鍚庣鍙戝竷
- </p>
- </div>
- </template>
-
- <div class="space-y-3">
- <div
- v-for="item in menuPreview"
- :key="item.key"
- class="flex items-center justify-between rounded-2xl border border-[var(--art-gray-200)] px-4 py-3"
- >
- <div>
- <p class="text-sm font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
- <p class="mt-1 text-xs text-[var(--art-gray-500)]">{{ item.description }}</p>
+ <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden">
+ <ElScrollbar>
+ <div
+ v-for="item in taskCardItems"
+ :key="`${item.time}-${item.title}`"
+ class="flex gap-4 border-b border-g-300 py-4 last:border-b-0"
+ >
+ <div class="flex flex-col items-center pt-1">
+ <span class="size-3 rounded-full bg-[var(--el-color-primary)]"></span>
+ <span class="mt-2 min-h-10 w-px bg-[var(--art-border-color)]"></span>
+ </div>
+ <div class="min-w-0 flex-1">
+ <p class="text-xs text-g-500">{{ item.time }}</p>
+ <div class="mt-2 flex items-center gap-2">
+ <p class="truncate text-base font-medium text-[var(--art-gray-900)]">
+ {{ item.title }}
+ </p>
+ <ElTag size="small" effect="light" :type="item.tagType">{{ item.tagText }}</ElTag>
+ </div>
+ <p class="mt-2 text-sm text-g-600">{{ item.subtitle }}</p>
+ </div>
</div>
- <ElTag :type="item.type" effect="light">
- {{ item.value }}
- </ElTag>
+ </ElScrollbar>
+ </div>
+ </div>
+
+ <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.deadStock">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>搴撳瓨鏈�杩戝姩鎬�</h4>
+ <p>{{ deadStockSubtitle }}</p>
</div>
</div>
- </ElCard>
+
+ <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden">
+ <ElScrollbar>
+ <div
+ v-for="item in stockCardItems"
+ :key="`${item.title}-${item.time}`"
+ class="flex items-center gap-4 border-b border-g-300 py-4 last:border-b-0"
+ >
+ <div class="size-12 rounded-2xl bg-[var(--el-color-primary-light-9)] flex-cc">
+ <ArtSvgIcon :icon="item.icon" class="text-xl text-[var(--el-color-primary)]" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <p class="truncate text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
+ <p class="mt-1 text-sm text-g-500">{{ item.status }}</p>
+ </div>
+ <div class="max-w-40 text-right text-sm text-g-500">{{ item.time }}</div>
+ </div>
+ </ElScrollbar>
+ </div>
+ </div>
</section>
</div>
</template>
@@ -147,15 +147,42 @@
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/modules/user'
- import { useMenuStore } from '@/store/modules/menu'
- import { formatMenuTitle } from '@/utils/router'
+ import {
+ fetchDashboardHeader,
+ fetchDashboardTrend,
+ fetchDashboardDeadStock,
+ fetchDashboardLocUsage,
+ fetchDashboardTasks
+ } from '@/api/dashboard'
+ import {
+ EMPTY_SUMMARY,
+ buildDashboardDeadStockQuery,
+ buildDashboardTaskQuery,
+ normalizeDashboardSummary,
+ normalizeDashboardTrend,
+ normalizeDashboardTaskList,
+ normalizeDashboardLocUsage,
+ normalizeDashboardDeadStockList,
+ withDashboardRequestGuard
+ } from './consolePage.helpers'
defineOptions({ name: 'Console' })
+ const summary = ref({ ...EMPTY_SUMMARY })
+ const trendModel = ref(normalizeDashboardTrend())
+ const taskList = ref([])
+ const deadStockList = ref([])
+ const locUsageList = ref([])
+ const sectionLoading = reactive({
+ summary: false,
+ trend: false,
+ deadStock: false,
+ locUsage: false,
+ tasks: false
+ })
+
const userStore = useUserStore()
- const menuStore = useMenuStore()
const { getUserInfo } = storeToRefs(userStore)
- const { menuList } = storeToRefs(menuStore)
const currentUser = computed(() => getUserInfo.value || {})
const currentUserName = computed(() => {
@@ -166,84 +193,191 @@
'RSF User'
)
})
- const currentRoles = computed(() => {
- const roles = currentUser.value.roles
- if (!Array.isArray(roles)) return []
- return roles
- .map((role) => {
- if (typeof role === 'string') {
- return role
- }
- return role?.code || role?.name || role?.title || ''
- })
- .filter(Boolean)
+ const currentDateText = computed(() => {
+ return new Date().toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })
})
- const currentAuthorities = computed(() => {
- const authorities = currentUser.value.authorities
- if (!Array.isArray(authorities)) return []
- return authorities.map((item) => item?.authority || '').filter(Boolean)
- })
- const currentMenuLabel = computed(() => {
- const firstMenu = menuList.value?.[0]
- return formatMenuTitle(firstMenu?.meta?.title || 'menus.dashboard.console')
- })
- const currentUserRoleText = computed(() => {
- if (!currentRoles.value.length) {
- return 'No role information yet'
+ const summaryCardItems = computed(() => [
+ {
+ title: '寰呭叆搴撴暟閲�',
+ count: summary.value.pendingIn,
+ metaLabel: '鎴嚦',
+ metaValue: currentDateText.value,
+ metaTone: 'text-g-500',
+ icon: 'ri:pie-chart-line',
+ iconBoxClass: 'bg-[var(--el-color-primary-light-9)]',
+ iconClass: 'text-[var(--el-color-primary)]'
+ },
+ {
+ title: '寰呭嚭搴撴暟閲�',
+ count: summary.value.pendingOut,
+ metaLabel: '鐘舵��',
+ metaValue: '褰撳墠寰呮墽琛屽嚭搴撻噺',
+ metaTone: 'text-success',
+ icon: 'ri:fire-line',
+ iconBoxClass: 'bg-[rgba(255,175,32,0.14)]',
+ iconClass: 'text-[#FFAF20]'
+ },
+ {
+ title: '宸插叆搴撴暟閲�',
+ count: summary.value.completedIn,
+ metaLabel: '缁撴灉',
+ metaValue: '绱瀹屾垚鍏ュ簱缁撴灉',
+ metaTone: 'text-g-500',
+ icon: 'ri:archive-line',
+ iconBoxClass: 'bg-[rgba(20,222,186,0.14)]',
+ iconClass: 'text-[#14DEBA]'
+ },
+ {
+ title: '鎵ц涓换鍔�',
+ count: summary.value.taskQty,
+ metaLabel: '褰撳墠鐢ㄦ埛',
+ metaValue: currentUserName.value,
+ metaTone: 'text-g-500',
+ icon: 'ri:progress-2-line',
+ iconBoxClass: 'bg-[rgba(139,92,246,0.16)]',
+ iconClass: 'text-[#8B5CF6]'
}
- return `Roles: ${currentRoles.value.join(' / ')}`
- })
- const backendSwitchCount = computed(() => {
- return currentUserName.value !== 'RSF User' || menuList.value.length > 0 ? 1 : 0
- })
- const visibleMenuCount = computed(() => countVisibleMenus(menuList.value))
- const permissionSignalCount = computed(() => {
- return currentRoles.value.length + currentAuthorities.value.length
- })
- const previewAuthorities = computed(() => currentAuthorities.value.slice(0, 4))
- const menuPreview = computed(() => {
- const total = visibleMenuCount.value
- const rootCount = Array.isArray(menuList.value) ? menuList.value.length : 0
- const firstMenu = menuList.value?.[0]
- const firstChildren = Array.isArray(firstMenu?.children) ? firstMenu.children.length : 0
+ ])
+ const trendDisplayModel = computed(() => {
+ const xAxisData = Array.isArray(trendModel.value.xAxisData) ? trendModel.value.xAxisData : []
+ const series = Array.isArray(trendModel.value.series) ? trendModel.value.series : []
+ if (!xAxisData.length || !series.length) {
+ return { xAxisData: [], series: [] }
+ }
- return [
- {
- key: 'entry',
- title: '鍏ュ彛妯″紡',
- description: '褰撳墠椤甸潰浠� backend mode 杩愯锛岃彍鍗曠敱鏈嶅姟绔┍鍔�',
- value: 'backend',
- type: 'success'
- },
- {
- key: 'menus',
- title: '鍙鑿滃崟',
- description: '宸查�氳繃鍔ㄦ�佽矾鐢遍�傞厤鍚庤繘鍏ュ墠绔彍鍗曟爲',
- value: `${total}`,
- type: 'primary'
- },
- {
- key: 'root',
- title: '涓�绾х洰褰�',
- description: '鏍圭骇鑿滃崟鑺傜偣鏁伴噺',
- value: `${rootCount}`,
- type: 'info'
- },
- {
- key: 'children',
- title: '棣栦釜鐩綍瀛愰」',
- description: '鐢ㄤ簬蹇�熺‘璁よ彍鍗曟爲宸茶姝g‘灞曞紑',
- value: `${firstChildren}`,
- type: 'warning'
+ const bucketSize = Math.max(1, Math.ceil(xAxisData.length / 8))
+ const bucketLabels = []
+ const bucketSeries = series.map((item) => ({
+ ...item,
+ data: []
+ }))
+
+ for (let index = 0; index < xAxisData.length; index += bucketSize) {
+ const labelBucket = xAxisData.slice(index, index + bucketSize)
+ bucketLabels.push(labelBucket[labelBucket.length - 1] || '--')
+ bucketSeries.forEach((seriesItem, seriesIndex) => {
+ const sourceBucket = Array.isArray(series[seriesIndex]?.data)
+ ? series[seriesIndex].data.slice(index, index + bucketSize)
+ : []
+ const bucketTotal = sourceBucket.reduce((total, value) => total + Number(value || 0), 0)
+ seriesItem.data.push(bucketTotal)
+ })
+ }
+
+ const visibleIndexes = bucketLabels.reduce((indexes, _, index) => {
+ const hasData = bucketSeries.some((item) => Number(item.data[index] || 0) > 0)
+ if (hasData) {
+ indexes.push(index)
}
- ]
+ return indexes
+ }, [])
+
+ const effectiveIndexes =
+ visibleIndexes.length > 0
+ ? visibleIndexes
+ : bucketLabels.map((_, index) => index).slice(-8)
+
+ return {
+ xAxisData: effectiveIndexes.map((index) => bucketLabels[index]),
+ series: bucketSeries.map((item) => ({
+ ...item,
+ data: effectiveIndexes.map((index) => item.data[index])
+ }))
+ }
+ })
+ const trendChartXAxisData = computed(() => trendDisplayModel.value.xAxisData)
+ const trendChartSeries = computed(() => trendDisplayModel.value.series)
+ const taskSubtitle = computed(() => `鏈�杩� ${taskList.value.length} 鏉′换鍔″姩鎬乣)
+ const deadStockSubtitle = computed(() => `鏈�杩� ${deadStockList.value.length} 鏉″簱瀛樿褰昤)
+ const usageLegendCount = computed(() => locUsageList.value.length)
+ const usageLegend = computed(() => {
+ const palette = ['#5B8FF9', '#5AD8A6', '#5D7092', '#F6BD16', '#E8684A', '#6DC8EC']
+ return locUsageList.value.map((item, index) => ({
+ ...item,
+ color: palette[index % palette.length]
+ }))
+ })
+ const taskCardItems = computed(() =>
+ taskList.value.slice(0, 6).map((item) => ({
+ title: item.title,
+ time: item.time,
+ subtitle: item.status,
+ tagText: resolveTaskTagText(item.status),
+ tagType: resolveTaskTagType(item.class)
+ }))
+ )
+ const stockCardItems = computed(() => deadStockList.value.slice(0, 6))
+
+ onMounted(() => {
+ loadDashboard()
})
- function countVisibleMenus(items) {
- if (!Array.isArray(items)) return 0
- return items.reduce((total, item) => {
- const current = item?.meta?.isHide ? 0 : 1
- return total + current + countVisibleMenus(item?.children)
- }, 0)
+ function loadDashboard() {
+ void loadSummarySection()
+ void loadTrendSection()
+ void loadDeadStockSection()
+ void loadLocUsageSection()
+ void loadTaskSection()
+ }
+
+ async function loadSummarySection() {
+ sectionLoading.summary = true
+ const payload = await withDashboardRequestGuard(fetchDashboardHeader(), null)
+ summary.value = normalizeDashboardSummary(payload)
+ sectionLoading.summary = false
+ }
+
+ async function loadTrendSection() {
+ sectionLoading.trend = true
+ const payload = await withDashboardRequestGuard(fetchDashboardTrend(), null)
+ trendModel.value = normalizeDashboardTrend(payload)
+ sectionLoading.trend = false
+ }
+
+ async function loadDeadStockSection() {
+ sectionLoading.deadStock = true
+ const payload = await withDashboardRequestGuard(
+ fetchDashboardDeadStock(buildDashboardDeadStockQuery()),
+ {}
+ )
+ deadStockList.value = normalizeDashboardDeadStockList(payload || {})
+ sectionLoading.deadStock = false
+ }
+
+ async function loadLocUsageSection() {
+ sectionLoading.locUsage = true
+ const payload = await withDashboardRequestGuard(fetchDashboardLocUsage(), null)
+ locUsageList.value = normalizeDashboardLocUsage(payload)
+ sectionLoading.locUsage = false
+ }
+
+ async function loadTaskSection() {
+ sectionLoading.tasks = true
+ const payload = await withDashboardRequestGuard(
+ fetchDashboardTasks(buildDashboardTaskQuery()),
+ {}
+ )
+ taskList.value = normalizeDashboardTaskList(payload || {})
+ sectionLoading.tasks = false
+ }
+
+ function resolveTaskTagType(statusClass) {
+ const text = String(statusClass || '')
+ if (text.includes('emerald')) return 'success'
+ if (text.includes('rose')) return 'danger'
+ return 'primary'
+ }
+
+ function resolveTaskTagText(statusText) {
+ const text = String(statusText || '')
+ .replace(/\b\d+\./g, '')
+ .replace(/\s+/g, ' ')
+ .trim()
+ const parts = text.split('路').map((item) => item.trim()).filter(Boolean)
+ return parts[parts.length - 1] || '澶勭悊涓�'
}
</script>
diff --git a/rsf-design/src/views/manager/freeze/freezePage.helpers.js b/rsf-design/src/views/manager/freeze/freezePage.helpers.js
new file mode 100644
index 0000000..f5ece80
--- /dev/null
+++ b/rsf-design/src/views/manager/freeze/freezePage.helpers.js
@@ -0,0 +1,98 @@
+export const FREEZE_DYNAMIC_FIELD_PREFIX = 'extendField__'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+export function createFreezeSearchState() {
+ return {
+ condition: '',
+ locCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ trackCode: ''
+ }
+}
+
+export function buildFreezePageQueryParams(params = {}) {
+ const result = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+
+ ;['condition', 'locCode', 'matnrCode', 'maktx', 'batch', 'trackCode'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function getFreezeDynamicFieldKey(fieldName) {
+ return `${FREEZE_DYNAMIC_FIELD_PREFIX}${fieldName}`
+}
+
+export function normalizeFreezeEnabledFields(fields = []) {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+ return fields
+ .map((item) => ({
+ fields: normalizeText(item.fields),
+ fieldsAlise: normalizeText(item.fieldsAlise || item.fieldsAlias || item.fields)
+ }))
+ .filter((item) => item.fields)
+}
+
+export function attachFreezeDynamicFields(record = {}, enabledFields = []) {
+ const extendFields = record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
+ const dynamicValues = {}
+
+ enabledFields.forEach((field) => {
+ dynamicValues[getFreezeDynamicFieldKey(field.fields)] = extendFields[field.fields] || ''
+ })
+
+ return {
+ ...record,
+ ...dynamicValues
+ }
+}
+
+export function normalizeFreezeRow(record = {}, enabledFields = []) {
+ return attachFreezeDynamicFields(
+ {
+ ...record,
+ locCode: record.locCode || '-',
+ wareArea: record.wareArea || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ batch: record.batch || '-',
+ trackCode: record.trackCode || '-',
+ unit: record.unit || '-',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ workQty: normalizeNumber(record.workQty),
+ splrName: record.splrName || '-',
+ statusText: record['status$'] || '鍐荤粨',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ memo: record.memo || '-'
+ },
+ enabledFields
+ )
+}
+
+export function normalizeFreezeDetail(record = {}, enabledFields = []) {
+ return normalizeFreezeRow(record, enabledFields)
+}
diff --git a/rsf-design/src/views/manager/freeze/freezeTable.columns.js b/rsf-design/src/views/manager/freeze/freezeTable.columns.js
new file mode 100644
index 0000000..f4f7d10
--- /dev/null
+++ b/rsf-design/src/views/manager/freeze/freezeTable.columns.js
@@ -0,0 +1,101 @@
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createFreezeTableColumns({ enabledFields = [] }) {
+ const dynamicColumns = enabledFields.map((field) => ({
+ prop: `extendField__${field.fields}`,
+ label: field.fieldsAlise,
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row[`extendField__${field.fields}`] || '-'
+ }))
+
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wareArea',
+ label: '搴撳尯',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'trackCode',
+ label: '杩借釜鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100
+ },
+ {
+ prop: 'anfme',
+ label: '鍙敤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '搴撳瓨鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120,
+ align: 'right'
+ },
+ ...dynamicColumns,
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'action',
+ label: '璇︽儏',
+ width: 100,
+ fixed: 'right',
+ useSlot: true,
+ align: 'center'
+ }
+ ]
+}
+
+export { ArtButtonTable }
diff --git a/rsf-design/src/views/manager/freeze/index.vue b/rsf-design/src/views/manager/freeze/index.vue
new file mode 100644
index 0000000..a7adef4
--- /dev/null
+++ b/rsf-design/src/views/manager/freeze/index.vue
@@ -0,0 +1,227 @@
+<template>
+ <div class="freeze-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ <template #action="{ row }">
+ <ArtButtonTable icon="ri:eye-line" @click="openDetailDrawer(row)" />
+ </template>
+ </ArtTable>
+ </ElCard>
+
+ <FreezeDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchEnabledFields, fetchFreezeDetail, fetchFreezePage } from '@/api/freeze'
+ import FreezeDetailDrawer from './modules/freeze-detail-drawer.vue'
+ import { createFreezeTableColumns } from './freezeTable.columns'
+ import {
+ buildFreezePageQueryParams,
+ createFreezeSearchState,
+ getFreezeDynamicFieldKey,
+ normalizeFreezeDetail,
+ normalizeFreezeEnabledFields,
+ normalizeFreezeRow
+ } from './freezePage.helpers'
+
+ defineOptions({ name: 'Freeze' })
+
+ const loading = ref(false)
+ const tableData = ref([])
+ const enabledFields = ref([])
+ const searchForm = ref(createFreezeSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�/鐗╂枡缂栫爜'
+ }
+ },
+ {
+ label: '搴撲綅缂栫爜',
+ key: 'locCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ },
+ {
+ label: '杩借釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ拷韪爜'
+ }
+ },
+ ...enabledFields.value.map((field) => ({
+ label: field.fieldsAlise,
+ key: getFreezeDynamicFieldKey(field.fields),
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: `璇疯緭鍏�${field.fieldsAlise}`
+ }
+ }))
+ ])
+
+ const { columns, columnChecks, resetColumns } = useTableColumns(() =>
+ createFreezeTableColumns({
+ enabledFields: enabledFields.value
+ })
+ )
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadEnabledFieldDefinitions() {
+ const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
+ timeoutMessage: '鎵╁睍瀛楁鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ enabledFields.value = normalizeFreezeEnabledFields(fields)
+ enabledFields.value.forEach((field) => {
+ const dynamicKey = getFreezeDynamicFieldKey(field.fields)
+ if (searchForm.value[dynamicKey] === undefined) {
+ searchForm.value[dynamicKey] = ''
+ }
+ })
+ resetColumns()
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchFreezePage(
+ buildFreezePageQueryParams({
+ ...searchForm.value,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: pagination.current,
+ size: pagination.size
+ },
+ { timeoutMessage: '鍐荤粨搴撳瓨鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeFreezeRow(record, enabledFields.value))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function openDetailDrawer(row) {
+ detailDrawerVisible.value = true
+ detailData.value = normalizeFreezeDetail(
+ await guardRequestWithMessage(fetchFreezeDetail(row.id), {}, {
+ timeoutMessage: '鍐荤粨搴撳瓨璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }),
+ enabledFields.value
+ )
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ searchForm.value = createFreezeSearchState()
+ enabledFields.value.forEach((field) => {
+ searchForm.value[getFreezeDynamicFieldKey(field.fields)] = ''
+ })
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ onMounted(async () => {
+ await loadEnabledFieldDefinitions()
+ await loadPageData()
+ })
+</script>
diff --git a/rsf-design/src/views/manager/freeze/modules/freeze-detail-drawer.vue b/rsf-design/src/views/manager/freeze/modules/freeze-detail-drawer.vue
new file mode 100644
index 0000000..0938e12
--- /dev/null
+++ b/rsf-design/src/views/manager/freeze/modules/freeze-detail-drawer.vue
@@ -0,0 +1,39 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鍐荤粨搴撳瓨璇︽儏"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯">{{ detail.wareArea || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="杩借釜鐮�">{{ detail.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙敤鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц涓暟閲�">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟�">{{ detail.splrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/in-statistic-item/inStatisticItemPage.helpers.js b/rsf-design/src/views/manager/in-statistic-item/inStatisticItemPage.helpers.js
new file mode 100644
index 0000000..c61ea0a
--- /dev/null
+++ b/rsf-design/src/views/manager/in-statistic-item/inStatisticItemPage.helpers.js
@@ -0,0 +1,86 @@
+import {
+ getInStatisticTaskStatusMeta,
+ getInStatisticTaskTypeMeta
+} from '../../statistics/in-statistic/inStatisticPage.helpers.js'
+
+export const IN_STATISTIC_ITEM_PAGE_TITLE = '鍏ュ簱缁熻鏄庣粏'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+export function createInStatisticItemSearchState() {
+ return {
+ condition: '',
+ dayTime: '',
+ maktx: '',
+ matnrCode: '',
+ batch: ''
+ }
+}
+
+export function getInStatisticItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildInStatisticItemSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ dayTime: normalizeText(params.dayTime),
+ maktx: normalizeText(params.maktx),
+ matnrCode: normalizeText(params.matnrCode),
+ batch: normalizeText(params.batch),
+ taskType: normalizeNumber(params.taskType, 1),
+ taskStatus: normalizeNumber(params.taskStatus, 100)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildInStatisticItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildInStatisticItemSearchParams(params)
+ }
+}
+
+export function normalizeInStatisticItemRow(record = {}) {
+ const taskTypeMeta = getInStatisticTaskTypeMeta(record.taskType ?? record.task_type)
+ const taskStatusMeta = getInStatisticTaskStatusMeta(record.taskStatus ?? record.task_status)
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ dayTimeText: normalizeText(record.dayTime || record.day_time || ''),
+ taskTypeText: normalizeText(record.taskTypeText || record['taskType$'] || taskTypeMeta.text),
+ taskTypeTagType: normalizeText(record.taskTypeTagType || taskTypeMeta.type) || 'info',
+ taskStatusText: normalizeText(record.taskStatusText || record['taskStatus$'] || taskStatusMeta.text),
+ taskStatusTagType: normalizeText(record.taskStatusTagType || taskStatusMeta.type) || 'info',
+ locCode: normalizeText(record.locCode || record.loc_code || ''),
+ barcode: normalizeText(record.barcode || ''),
+ matnrCode: normalizeText(record.matnrCode || record.matnr_code || ''),
+ maktx: normalizeText(record.maktx || ''),
+ batch: normalizeText(record.batch || ''),
+ unit: normalizeText(record.unit || ''),
+ anfme: record.anfme ?? '--',
+ fieldsIndex: normalizeText(record.fieldsIndex || record.fields_index || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || record.createBy || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || record.updateBy || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '')
+ }
+}
diff --git a/rsf-design/src/views/manager/in-statistic-item/inStatisticItemTable.columns.js b/rsf-design/src/views/manager/in-statistic-item/inStatisticItemTable.columns.js
new file mode 100644
index 0000000..4c49e25
--- /dev/null
+++ b/rsf-design/src/views/manager/in-statistic-item/inStatisticItemTable.columns.js
@@ -0,0 +1,117 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createInStatisticItemTableColumns({ handleView } = {}) {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'id',
+ label: 'ID',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.id ?? '--'
+ },
+ {
+ prop: 'dayTimeText',
+ label: '缁熻鏃ユ湡',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.dayTimeText || '--'
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locCode || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'barcode',
+ label: '鎵樼洏鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.barcode || '--'
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/in-statistic-item/index.vue b/rsf-design/src/views/manager/in-statistic-item/index.vue
new file mode 100644
index 0000000..1809adb
--- /dev/null
+++ b/rsf-design/src/views/manager/in-statistic-item/index.vue
@@ -0,0 +1,157 @@
+<template>
+ <div class="in-statistic-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <InStatisticItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchGetInStatisticItemDetail, fetchInStatisticItemPage } from '@/api/in-statistic-item'
+ import {
+ buildInStatisticItemPageQueryParams,
+ buildInStatisticItemSearchParams,
+ createInStatisticItemSearchState,
+ getInStatisticItemPaginationKey,
+ normalizeInStatisticItemRow
+ } from './inStatisticItemPage.helpers'
+ import { createInStatisticItemTableColumns } from './inStatisticItemTable.columns'
+ import InStatisticItemDetailDrawer from './modules/in-statistic-item-detail-drawer.vue'
+
+ defineOptions({ name: 'InStatisticItem' })
+
+ const searchForm = ref(createInStatisticItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�/缂栫爜/鎵规'
+ }
+ },
+ {
+ label: '缁熻鏃ユ湡',
+ key: 'dayTime',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ const normalizedRow = normalizeInStatisticItemRow(row)
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ detailData.value = normalizedRow
+
+ try {
+ if (row?.id === void 0 || row?.id === null || row?.id === '') {
+ return
+ }
+ const response = await fetchGetInStatisticItemDetail(row.id)
+ const detail = response?.data?.data ?? response?.data ?? response ?? {}
+ detailData.value = normalizeInStatisticItemRow(detail)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchInStatisticItemPage,
+ apiParams: buildInStatisticItemPageQueryParams(searchForm.value),
+ paginationKey: getInStatisticItemPaginationKey(),
+ columnsFactory: () =>
+ createInStatisticItemTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeInStatisticItemRow(item)) : []
+ }
+ })
+
+ function handleSearch(params) {
+ replaceSearchParams(buildInStatisticItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createInStatisticItemSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/manager/in-statistic-item/modules/in-statistic-item-detail-drawer.vue b/rsf-design/src/views/manager/in-statistic-item/modules/in-statistic-item-detail-drawer.vue
new file mode 100644
index 0000000..5c2afed
--- /dev/null
+++ b/rsf-design/src/views/manager/in-statistic-item/modules/in-statistic-item-detail-drawer.vue
@@ -0,0 +1,58 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ :title="IN_STATISTIC_ITEM_PAGE_TITLE + '璇︽儏'"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElSkeleton v-if="loading" :rows="10" animated />
+ <ElDescriptions v-else :column="4" border>
+ <ElDescriptionsItem label="ID">{{ detail.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁熻鏃ユ湡">{{ detail.dayTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟绫诲瀷">
+ <ElTag :type="detail.taskTypeTagType || 'info'" effect="light">
+ {{ detail.taskTypeText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鐘舵��">
+ <ElTag :type="detail.taskStatusTagType || 'info'" effect="light">
+ {{ detail.taskStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { IN_STATISTIC_ITEM_PAGE_TITLE } from '../inStatisticItemPage.helpers'
+
+ defineOptions({ name: 'InStatisticItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/loc-dead-report/index.vue b/rsf-design/src/views/manager/loc-dead-report/index.vue
new file mode 100644
index 0000000..4ec4b8b
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-dead-report/index.vue
@@ -0,0 +1,402 @@
+<template>
+ <div class="loc-dead-report-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <LocDeadReportDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchExportLocDeadReport,
+ fetchGetLocDeadReportDetail,
+ fetchGetLocDeadReportMany,
+ fetchLocDeadReportPage
+ } from '@/api/loc-dead-report'
+ import LocDeadReportDetailDrawer from './modules/loc-dead-report-detail-drawer.vue'
+ import { createLocDeadReportTableColumns } from './locDeadReportTable.columns'
+ import {
+ LOC_DEAD_REPORT_TITLE,
+ buildLocDeadReportPageQueryParams,
+ buildLocDeadReportPrintRows,
+ createLocDeadReportSearchState,
+ getLocDeadReportPaginationKey,
+ getLocDeadReportReportColumns,
+ normalizeLocDeadReportDetail,
+ normalizeLocDeadReportRow
+ } from './locDeadReportPage.helpers'
+
+ defineOptions({ name: 'LocDeadReport' })
+
+ const userStore = useUserStore()
+ const reportTitle = LOC_DEAD_REPORT_TITLE
+ const searchForm = ref(createLocDeadReportSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const reportColumns = getLocDeadReportReportColumns()
+
+ const reportQueryParams = computed(() => buildLocDeadReportPageQueryParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�/鐗╂枡缂栫爜/澶囨敞'
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ },
+ {
+ label: '搴撲綅缂栫爜',
+ key: 'locCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�'
+ }
+ },
+ {
+ label: '搴撲綅ID',
+ key: 'locId',
+ type: 'number',
+ props: {
+ clearable: true,
+ controls: false,
+ placeholder: '璇疯緭鍏ュ簱浣岻D'
+ }
+ },
+ {
+ label: '鍗曟嵁ID',
+ key: 'orderId',
+ type: 'number',
+ props: {
+ clearable: true,
+ controls: false,
+ placeholder: '璇疯緭鍏ュ崟鎹甀D'
+ }
+ },
+ {
+ label: '鍗曟嵁绫诲瀷',
+ key: 'type',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟鎹被鍨�'
+ }
+ },
+ {
+ label: '璁㈠崟鏄庣粏ID',
+ key: 'orderItemId',
+ type: 'number',
+ props: {
+ clearable: true,
+ controls: false,
+ placeholder: '璇疯緭鍏ヨ鍗曟槑缁咺D'
+ }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'number',
+ props: {
+ clearable: true,
+ controls: false,
+ placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�'
+ }
+ },
+ {
+ label: '鐗╂枡ID',
+ key: 'matnrId',
+ type: 'number',
+ props: {
+ clearable: true,
+ controls: false,
+ placeholder: '璇疯緭鍏ョ墿鏂橧D'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '璺熻釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ窡韪爜'
+ }
+ },
+ {
+ label: '鍗曚綅',
+ key: 'unit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟浣�'
+ }
+ },
+ {
+ label: '鏁伴噺',
+ key: 'anfme',
+ type: 'number',
+ props: {
+ clearable: true,
+ controls: false,
+ placeholder: '璇疯緭鍏ユ暟閲�'
+ }
+ },
+ {
+ label: '搴撳瓨鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樻壒娆�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '瑙勬牸',
+ key: 'spec',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鏍�'
+ }
+ },
+ {
+ label: '鍨嬪彿',
+ key: 'model',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瀷鍙�'
+ }
+ },
+ {
+ label: '瀛楁绱㈠紩',
+ key: 'fieldsIndex',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈电储寮�'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchLocDeadReportPage,
+ apiParams: buildLocDeadReportPageQueryParams(searchForm.value),
+ paginationKey: getLocDeadReportPaginationKey(),
+ columnsFactory: () =>
+ createLocDeadReportTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeLocDeadReportRow(item)) : [])
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetLocDeadReportMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchLocDeadReportPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'loc-dead-report.xlsx',
+ requestExport: (payload) =>
+ fetchExportLocDeadReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildLocDeadReportPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetLocDeadReportDetail(id), {}, {
+ timeoutMessage: '搴撳瓨鍋滄粸璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeLocDeadReportDetail({
+ ...fallback,
+ ...detail
+ })
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildLocDeadReportPageQueryParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createLocDeadReportSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/manager/loc-dead-report/locDeadReportPage.helpers.js b/rsf-design/src/views/manager/loc-dead-report/locDeadReportPage.helpers.js
new file mode 100644
index 0000000..6cffe2a
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-dead-report/locDeadReportPage.helpers.js
@@ -0,0 +1,198 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const LOC_DEAD_REPORT_TITLE = '搴撳瓨鍋滄粸鎶ヨ〃'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+export function createLocDeadReportSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ locCode: '',
+ locId: '',
+ orderId: '',
+ type: '',
+ orderItemId: '',
+ wkType: '',
+ matnrId: '',
+ maktx: '',
+ matnrCode: '',
+ trackCode: '',
+ unit: '',
+ anfme: '',
+ batch: '',
+ splrBatch: '',
+ spec: '',
+ model: '',
+ fieldsIndex: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getLocDeadReportPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildLocDeadReportSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'locCode',
+ 'type',
+ 'maktx',
+ 'matnrCode',
+ 'trackCode',
+ 'unit',
+ 'batch',
+ 'splrBatch',
+ 'spec',
+ 'model',
+ 'fieldsIndex',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['locId', 'orderId', 'orderItemId', 'wkType', 'matnrId', 'anfme', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function buildLocDeadReportPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ orderBy: params.orderBy || 'create_time asc',
+ ...buildLocDeadReportSearchParams(params)
+ }
+}
+
+export function getLocDeadReportStatusMeta(value) {
+ return STATUS_META[Number(value)] || { text: '-', type: 'info' }
+}
+
+export function normalizeLocDeadReportRow(record = {}) {
+ const statusMeta = getLocDeadReportStatusMeta(record.status)
+ return {
+ ...record,
+ id: record.id ?? '--',
+ locCode: record.locCode || '--',
+ deadTime: record.deadTime ?? record['deadTime$'] ?? '--',
+ locId: record.locId ?? '--',
+ orderId: record.orderId ?? '--',
+ typeText: record['type$'] || record.type || '--',
+ orderItemId: record.orderItemId ?? '--',
+ wkTypeText: record['wkType$'] || record.wkType || '--',
+ matnrId: record.matnrId ?? '--',
+ matnrCode: record.matnrCode || '--',
+ maktx: record.maktx || '--',
+ trackCode: record.trackCode || '--',
+ unit: record.unit || '--',
+ anfme: record.anfme ?? '--',
+ batch: record.batch || '--',
+ splrBatch: record.splrBatch || '--',
+ spec: record.spec || '--',
+ model: record.model || '--',
+ fieldsIndex: record.fieldsIndex || '--',
+ createByText: record['createBy$'] || record.createByText || '--',
+ createTimeText: record['createTime$'] || record.createTime || '--',
+ updateByText: record['updateBy$'] || record.updateByText || '--',
+ updateTimeText: record['updateTime$'] || record.updateTime || '--',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ memo: record.memo || '--'
+ }
+}
+
+export function normalizeLocDeadReportDetail(record = {}) {
+ return normalizeLocDeadReportRow(record)
+}
+
+export function getLocDeadReportReportColumns() {
+ return [
+ { source: 'locCode', label: '搴撲綅缂栫爜' },
+ { source: 'deadTime', label: '鍋滅暀鏃堕棿锛堝ぉ锛�', align: 'right' },
+ { source: 'locId', label: '搴撲綅ID', align: 'right' },
+ { source: 'orderId', label: '鍗曟嵁ID', align: 'right' },
+ { source: 'typeText', label: '鍗曟嵁绫诲瀷' },
+ { source: 'orderItemId', label: '璁㈠崟鏄庣粏ID', align: 'right' },
+ { source: 'wkTypeText', label: '涓氬姟绫诲瀷' },
+ { source: 'matnrId', label: '鐗╂枡ID', align: 'right' },
+ { source: 'matnrCode', label: '鐗╂枡缂栫爜' },
+ { source: 'maktx', label: '鐗╂枡鍚嶇О' },
+ { source: 'trackCode', label: '鐗╂枡璺熻釜鐮�' },
+ { source: 'unit', label: '鍗曚綅' },
+ { source: 'anfme', label: '鏁伴噺', align: 'right' },
+ { source: 'batch', label: '搴撳瓨鎵规' },
+ { source: 'splrBatch', label: '渚涘簲鍟嗘壒娆�' },
+ { source: 'spec', label: '瑙勬牸' },
+ { source: 'model', label: '鍨嬪彿' },
+ { source: 'fieldsIndex', label: '瀛楁绱㈠紩' },
+ { source: 'statusText', label: '鐘舵��' },
+ { source: 'createTimeText', label: '鍒涘缓鏃堕棿' },
+ { source: 'updateTimeText', label: '鏇存柊鏃堕棿' }
+ ]
+}
+
+export function buildLocDeadReportPrintRows(records = []) {
+ return records.map((record) => {
+ const row = normalizeLocDeadReportRow(record)
+ return {
+ locCode: row.locCode,
+ deadTime: row.deadTime,
+ locId: row.locId,
+ orderId: row.orderId,
+ typeText: row.typeText,
+ orderItemId: row.orderItemId,
+ wkTypeText: row.wkTypeText,
+ matnrId: row.matnrId,
+ matnrCode: row.matnrCode,
+ maktx: row.maktx,
+ trackCode: row.trackCode,
+ unit: row.unit,
+ anfme: row.anfme,
+ batch: row.batch,
+ splrBatch: row.splrBatch,
+ spec: row.spec,
+ model: row.model,
+ fieldsIndex: row.fieldsIndex,
+ statusText: row.statusText,
+ createTimeText: row.createTimeText,
+ updateTimeText: row.updateTimeText
+ }
+ })
+}
diff --git a/rsf-design/src/views/manager/loc-dead-report/locDeadReportTable.columns.js b/rsf-design/src/views/manager/loc-dead-report/locDeadReportTable.columns.js
new file mode 100644
index 0000000..da7484a
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-dead-report/locDeadReportTable.columns.js
@@ -0,0 +1,150 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createLocDeadReportTableColumns({ handleView } = {}) {
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'deadTime',
+ label: '鍋滅暀鏃堕棿锛堝ぉ锛�',
+ width: 118,
+ align: 'right',
+ formatter: (row) => row?.deadTime ?? '--'
+ },
+ {
+ prop: 'locId',
+ label: '搴撲綅ID',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'typeText',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wkTypeText',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'trackCode',
+ label: '璺熻釜鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'batch',
+ label: '搴撳瓨鎵规',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'spec',
+ label: '瑙勬牸',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'model',
+ label: '鍨嬪彿',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '瀛楁绱㈠紩',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.statusType || 'info',
+ effect: 'light'
+ },
+ () => row?.statusText || '--'
+ )
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
+
+export { ArtButtonTable }
diff --git a/rsf-design/src/views/manager/loc-dead-report/modules/loc-dead-report-detail-drawer.vue b/rsf-design/src/views/manager/loc-dead-report/modules/loc-dead-report-detail-drawer.vue
new file mode 100644
index 0000000..1d55d55
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-dead-report/modules/loc-dead-report-detail-drawer.vue
@@ -0,0 +1,82 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳瓨鍋滄粸璇︽儏"
+ size="80%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2" v-loading="loading">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍋滅暀鏃堕棿锛堝ぉ锛�">{{ detail.deadTime ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅ID">{{ detail.locId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁ID">{{ detail.orderId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detail.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁㈠崟鏄庣粏ID">{{ detail.orderItemId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡ID">{{ detail.matnrId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璺熻釜鐮�">{{ detail.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勬牸">{{ detail.spec || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍨嬪彿">{{ detail.model || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div v-if="extendFieldEntries.length > 0" class="flex min-h-0 flex-col gap-2">
+ <div class="text-sm font-medium text-[var(--art-gray-700)]">鎵╁睍瀛楁</div>
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem
+ v-for="entry in extendFieldEntries"
+ :key="entry[0]"
+ :label="entry[0]"
+ >
+ {{ entry[1] || '--' }}
+ </ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ defineOptions({ name: 'LocDeadReportDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const extendFieldEntries = computed(() => {
+ const extendFields = props.detail?.extendFields
+ if (!extendFields || typeof extendFields !== 'object') {
+ return []
+ }
+ return Object.entries(extendFields)
+ })
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/loc-item/index.vue b/rsf-design/src/views/manager/loc-item/index.vue
new file mode 100644
index 0000000..f6a40ed
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-item/index.vue
@@ -0,0 +1,234 @@
+<template>
+ <div class="loc-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ <template #action="{ row }">
+ <ArtButtonTable icon="ri:eye-line" @click="openDetail(row)" />
+ </template>
+ </ArtTable>
+ </ElCard>
+
+ <LocItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :detail="detailData"
+ :enabled-fields="detailFieldConfigs"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { useRoute } from 'vue-router'
+ import { ElMessage } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { fetchEnabledFields, fetchLocItemDetail, fetchLocItemPage } from '@/api/loc-item'
+ import LocItemDetailDrawer from './modules/loc-item-detail-drawer.vue'
+ import { createLocItemTableColumns } from './locItemTable.columns'
+ import {
+ buildLocItemPageQueryParams,
+ buildLocItemSearchParams,
+ createLocItemSearchState,
+ getLocItemDynamicFieldKey,
+ getLocItemPaginationKey,
+ getLocItemStatusOptions,
+ normalizeLocItemEnabledFields,
+ normalizeLocItemRow
+ } from './locItemPage.helpers'
+
+ defineOptions({ name: 'LocItem' })
+
+ const route = useRoute()
+ const searchForm = ref(createLocItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const enabledFields = ref([])
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�/鐗╂枡缂栫爜/杩借釜鐮�' }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
+ },
+ {
+ label: '搴撲綅ID',
+ key: 'locId',
+ type: 'inputNumber',
+ props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ュ簱浣岻D' }
+ },
+ {
+ label: '鍗曟嵁ID',
+ key: 'orderId',
+ type: 'inputNumber',
+ props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ュ崟鎹甀D' }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'type',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�' }
+ },
+ {
+ label: '宸ヤ綅绫诲瀷',
+ key: 'wkType',
+ type: 'inputNumber',
+ props: { min: 0, controlsPosition: 'right', placeholder: '璇疯緭鍏ュ伐浣嶇被鍨�' }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�' }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�' }
+ },
+ {
+ label: '杩借釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヨ拷韪爜' }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ壒娆�' }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规' }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: { clearable: true, options: getLocItemStatusOptions() }
+ }
+ ])
+
+ const buildFieldConfigs = () =>
+ enabledFields.value.map((field) => ({
+ prop: getLocItemDynamicFieldKey(field.fields),
+ label: field.fieldsAlise
+ }))
+
+ const detailFieldConfigs = computed(() => buildFieldConfigs())
+
+ const { columns, columnChecks, resetColumns } = useTableColumns(() =>
+ createLocItemTableColumns({
+ enabledFields: buildFieldConfigs(),
+ handleView: openDetail
+ })
+ )
+
+ const {
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: (params) =>
+ guardRequestWithMessage(
+ fetchLocItemPage(params),
+ {
+ records: [],
+ total: 0,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ },
+ { timeoutMessage: '搴撳瓨鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ ),
+ apiParams: buildLocItemPageQueryParams(searchForm.value),
+ paginationKey: getLocItemPaginationKey()
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeLocItemRow(item, enabledFields.value)) : []
+ }
+ })
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ try {
+ detailData.value = normalizeLocItemRow(await fetchLocItemDetail(row.id), enabledFields.value)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撳瓨鏄庣粏璇︽儏澶辫触')
+ }
+ }
+
+ async function loadEnabledFieldDefinitions() {
+ const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
+ timeoutMessage: '鎵╁睍瀛楁鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ enabledFields.value = normalizeLocItemEnabledFields(fields)
+ resetColumns()
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildLocItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ const nextState = createLocItemSearchState()
+ if (route.query.locId) {
+ nextState.locId = route.query.locId
+ }
+ Object.assign(searchForm.value, nextState)
+ resetSearchParams()
+ }
+
+ onMounted(async () => {
+ if (route.query.locId) {
+ searchForm.value.locId = route.query.locId
+ replaceSearchParams(buildLocItemSearchParams(searchForm.value))
+ }
+ await loadEnabledFieldDefinitions()
+ await getData()
+ })
+</script>
diff --git a/rsf-design/src/views/manager/loc-item/locItemPage.helpers.js b/rsf-design/src/views/manager/loc-item/locItemPage.helpers.js
new file mode 100644
index 0000000..0bc729d
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-item/locItemPage.helpers.js
@@ -0,0 +1,169 @@
+export const LOC_ITEM_DYNAMIC_FIELD_PREFIX = 'extendField__'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+export function createLocItemSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ locId: '',
+ orderId: '',
+ type: '',
+ orderItemId: '',
+ wkType: '',
+ matnrId: '',
+ maktx: '',
+ matnrCode: '',
+ trackCode: '',
+ unit: '',
+ anfme: '',
+ batch: '',
+ splrBatch: '',
+ spec: '',
+ model: '',
+ fieldsIndex: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getLocItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildLocItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'type',
+ 'maktx',
+ 'matnrCode',
+ 'trackCode',
+ 'unit',
+ 'batch',
+ 'splrBatch',
+ 'spec',
+ 'model',
+ 'fieldsIndex',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['locId', 'orderId', 'orderItemId', 'wkType', 'matnrId', 'anfme', 'status'].forEach((key) => {
+ if (params[key] === '' || params[key] === null || params[key] === undefined) {
+ return
+ }
+ const numericValue = Number(params[key])
+ if (Number.isFinite(numericValue)) {
+ result[key] = numericValue
+ }
+ })
+
+ if (params.timeStart) {
+ result.timeStart = params.timeStart
+ }
+ if (params.timeEnd) {
+ result.timeEnd = params.timeEnd
+ }
+
+ return result
+}
+
+export function buildLocItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildLocItemSearchParams(params)
+ }
+}
+
+export function getLocItemStatusOptions() {
+ return [
+ { label: '鍚敤', value: 1 },
+ { label: '鍋滅敤', value: 0 }
+ ]
+}
+
+export function getLocItemDynamicFieldKey(fieldName) {
+ return `${LOC_ITEM_DYNAMIC_FIELD_PREFIX}${fieldName}`
+}
+
+export function normalizeLocItemEnabledFields(fields = []) {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+ return fields
+ .map((item) => ({
+ fields: normalizeText(item.fields),
+ fieldsAlise: normalizeText(item.fieldsAlise || item.fieldsAlias || item.fields)
+ }))
+ .filter((item) => item.fields)
+}
+
+export function attachLocItemDynamicFields(record = {}, enabledFields = []) {
+ const extendFields = record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
+ const dynamicValues = {}
+
+ enabledFields.forEach((field) => {
+ dynamicValues[getLocItemDynamicFieldKey(field.fields)] = extendFields[field.fields] || ''
+ })
+
+ return {
+ ...record,
+ ...dynamicValues
+ }
+}
+
+export function normalizeLocItemRow(record = {}, enabledFields = []) {
+ return attachLocItemDynamicFields(
+ {
+ ...record,
+ locId: record.locId ?? '--',
+ locCode: record.locCode || '--',
+ wareArea: record.wareArea || '--',
+ orderId: record.orderId ?? '--',
+ orderItemId: record.orderItemId ?? '--',
+ matnrId: record.matnrId ?? '--',
+ typeText: record['type$'] || record.type || '--',
+ wkTypeText: record['wkType$'] || record.wkType || '--',
+ matnrCode: record.matnrCode || '--',
+ maktx: record.maktx || '--',
+ spec: record.spec || '--',
+ model: record.model || '--',
+ batch: record.batch || '--',
+ splrBatch: record.splrBatch || '--',
+ trackCode: record.trackCode || '--',
+ unit: record.unit || '--',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ workQty: normalizeNumber(record.workQty),
+ fieldsIndex: record.fieldsIndex || '--',
+ memo: record.memo || '--',
+ statusText: record.statusBool || Number(record.status) === 1 ? '鍚敤' : '鍋滅敤',
+ createByText: record['createBy$'] || record.createBy || '--',
+ createTimeText: record['createTime$'] || record.createTime || '--',
+ updateByText: record['updateBy$'] || record.updateBy || '--',
+ updateTimeText: record['updateTime$'] || record.updateTime || '--'
+ },
+ enabledFields
+ )
+}
diff --git a/rsf-design/src/views/manager/loc-item/locItemTable.columns.js b/rsf-design/src/views/manager/loc-item/locItemTable.columns.js
new file mode 100644
index 0000000..4a73ded
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-item/locItemTable.columns.js
@@ -0,0 +1,143 @@
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createLocItemTableColumns({ enabledFields = [], handleView }) {
+ return [
+ {
+ prop: 'locId',
+ label: '搴撲綅ID',
+ width: 110
+ },
+ {
+ prop: 'wareArea',
+ label: '搴撳尯',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'typeText',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wkTypeText',
+ label: '宸ヤ綅绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orderId',
+ label: '鍗曟嵁ID',
+ minWidth: 120
+ },
+ {
+ prop: 'orderItemId',
+ label: '鍗曟嵁鏄庣粏ID',
+ minWidth: 130
+ },
+ {
+ prop: 'matnrId',
+ label: '鐗╂枡ID',
+ minWidth: 110
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'spec',
+ label: '瑙勬牸',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'model',
+ label: '鍨嬪彿',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'trackCode',
+ label: '杩借釜鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100
+ },
+ {
+ prop: 'anfme',
+ label: '鍙敤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '搴撳瓨鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120,
+ align: 'right'
+ },
+ ...enabledFields.map((field) => ({
+ prop: field.prop,
+ label: field.label,
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row[field.prop] || '-'
+ })),
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 90
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'action',
+ label: '璇︽儏',
+ width: 96,
+ fixed: 'right',
+ align: 'center',
+ useSlot: true
+ }
+ ]
+}
+
+export { ArtButtonTable }
diff --git a/rsf-design/src/views/manager/loc-item/modules/loc-item-detail-drawer.vue b/rsf-design/src/views/manager/loc-item/modules/loc-item-detail-drawer.vue
new file mode 100644
index 0000000..e8a75a7
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-item/modules/loc-item-detail-drawer.vue
@@ -0,0 +1,71 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳瓨鏄庣粏璇︽儏"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="搴撲綅ID">{{ detail.locId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯">{{ detail.wareArea || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ヤ綅绫诲瀷">{{ detail.wkTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁ID">{{ detail.orderId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="杩借釜鐮�">{{ detail.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙敤鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц涓暟閲�">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElCard shadow="never">
+ <template #header>
+ <div class="text-sm font-medium">鎵╁睍瀛楁</div>
+ </template>
+ <div v-if="dynamicFields.length" class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
+ <div
+ v-for="field in dynamicFields"
+ :key="field.prop"
+ class="rounded-lg border border-[var(--el-border-color-light)] px-3 py-2"
+ >
+ <div class="text-xs text-[var(--art-gray-500)]">{{ field.label }}</div>
+ <div class="mt-1 text-sm text-[var(--art-text-gray-900)]">{{ field.value || '--' }}</div>
+ </div>
+ </div>
+ <ElEmpty v-else description="鏆傛棤鎵╁睍瀛楁" :image-size="84" />
+ </ElCard>
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ enabledFields: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const dynamicFields = computed(() =>
+ props.enabledFields.map((field) => ({
+ ...field,
+ value: props.detail[field.prop]
+ }))
+ )
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/loc-preview/index.vue b/rsf-design/src/views/manager/loc-preview/index.vue
new file mode 100644
index 0000000..c3a0bf6
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-preview/index.vue
@@ -0,0 +1,333 @@
+<template>
+ <div class="loc-preview-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ <template #action="{ row }">
+ <ArtButtonTable icon="ri:eye-line" @click="openDetailDrawer(row)" />
+ </template>
+ </ArtTable>
+ </ElCard>
+
+ <LocPreviewDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="activeLocDetail"
+ :data="detailTableData"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @refresh="loadDetailResources"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchEnabledFields,
+ fetchLocPreviewDetail,
+ fetchLocPreviewItemsPage,
+ fetchLocPreviewPage
+ } from '@/api/loc-preview'
+ import LocPreviewDetailDrawer from './modules/loc-preview-detail-drawer.vue'
+ import { createLocPreviewTableColumns } from './locPreviewTable.columns'
+ import {
+ buildLocPreviewPageQueryParams,
+ createLocPreviewSearchState,
+ getLocPreviewDynamicFieldKey,
+ normalizeLocPreviewDetail,
+ normalizeLocPreviewEnabledFields,
+ normalizeLocPreviewItemRow,
+ normalizeLocPreviewRow
+ } from './locPreviewPage.helpers'
+
+ defineOptions({ name: 'LocPreview' })
+
+ const loading = ref(false)
+ const detailLoading = ref(false)
+ const tableData = ref([])
+ const detailTableData = ref([])
+ const detailDrawerVisible = ref(false)
+ const activeLocRow = ref(null)
+ const activeLocDetail = ref({})
+ const enabledFields = ref([])
+ const searchForm = ref(createLocPreviewSearchState())
+
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�/鏉$爜'
+ }
+ },
+ {
+ label: '搴撲綅缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�'
+ }
+ },
+ {
+ label: '鏉$爜',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯鐮�'
+ }
+ }
+ ])
+
+ function createDetailColumns() {
+ return [
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wareArea',
+ label: '搴撳尯',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orderCode',
+ label: '鍗曟嵁缂栧彿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'trackCode',
+ label: '杩借釜鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100
+ },
+ {
+ prop: 'anfme',
+ label: '鍙敤鏁伴噺',
+ width: 120
+ },
+ {
+ prop: 'qty',
+ label: '搴撳瓨鏁伴噺',
+ width: 120
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120
+ },
+ ...enabledFields.value.map((field) => ({
+ prop: getLocPreviewDynamicFieldKey(field.fields),
+ label: field.fieldsAlise,
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row[getLocPreviewDynamicFieldKey(field.fields)] || '-'
+ })),
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ }
+ ]
+ }
+
+ const detailColumns = computed(() => createDetailColumns())
+
+ const { columns, columnChecks } = useTableColumns(() =>
+ createLocPreviewTableColumns({
+ handleViewDetail: openDetailDrawer
+ })
+ )
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadEnabledFieldDefinitions() {
+ const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
+ timeoutMessage: '鎵╁睍瀛楁鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ enabledFields.value = normalizeLocPreviewEnabledFields(fields)
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchLocPreviewPage(
+ buildLocPreviewPageQueryParams({
+ ...searchForm.value,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: pagination.current,
+ size: pagination.size
+ },
+ { timeoutMessage: '搴撲綅鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeLocPreviewRow(record))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function loadDetailResources() {
+ if (!activeLocRow.value?.id) {
+ return
+ }
+
+ detailLoading.value = true
+ try {
+ const [detailResponse, itemResponse] = await Promise.all([
+ guardRequestWithMessage(fetchLocPreviewDetail(activeLocRow.value.id), {}, {
+ timeoutMessage: '搴撲綅璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }),
+ guardRequestWithMessage(
+ fetchLocPreviewItemsPage({
+ current: detailPagination.current,
+ pageSize: detailPagination.size,
+ locId: activeLocRow.value.id
+ }),
+ {
+ records: [],
+ total: 0,
+ current: detailPagination.current,
+ size: detailPagination.size
+ },
+ { timeoutMessage: '搴撲綅搴撳瓨鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ ])
+
+ activeLocDetail.value = normalizeLocPreviewDetail(detailResponse)
+ detailTableData.value = Array.isArray(itemResponse?.records)
+ ? itemResponse.records.map((record) => normalizeLocPreviewItemRow(record, enabledFields.value))
+ : []
+ updatePaginationState(detailPagination, itemResponse, detailPagination.current, detailPagination.size)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function openDetailDrawer(row) {
+ activeLocRow.value = row
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ loadDetailResources()
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ searchForm.value = createLocPreviewSearchState()
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ detailPagination.current = 1
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ onMounted(async () => {
+ await loadEnabledFieldDefinitions()
+ await loadPageData()
+ })
+</script>
diff --git a/rsf-design/src/views/manager/loc-preview/locPreviewPage.helpers.js b/rsf-design/src/views/manager/loc-preview/locPreviewPage.helpers.js
new file mode 100644
index 0000000..43e6b35
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-preview/locPreviewPage.helpers.js
@@ -0,0 +1,127 @@
+export const LOC_PREVIEW_DYNAMIC_FIELD_PREFIX = 'extendField__'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+export function createLocPreviewSearchState() {
+ return {
+ condition: '',
+ code: '',
+ barcode: ''
+ }
+}
+
+export function buildLocPreviewPageQueryParams(params = {}) {
+ const result = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+
+ const condition = normalizeText(params.condition)
+ const code = normalizeText(params.code)
+ const barcode = normalizeText(params.barcode)
+
+ if (condition) {
+ result.condition = condition
+ }
+ if (code) {
+ result.code = code
+ }
+ if (barcode) {
+ result.barcode = barcode
+ }
+
+ return result
+}
+
+export function getLocPreviewDynamicFieldKey(fieldName) {
+ return `${LOC_PREVIEW_DYNAMIC_FIELD_PREFIX}${fieldName}`
+}
+
+export function normalizeLocPreviewEnabledFields(fields = []) {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+ return fields
+ .map((item) => ({
+ fields: normalizeText(item.fields),
+ fieldsAlise: normalizeText(item.fieldsAlise || item.fieldsAlias || item.fields)
+ }))
+ .filter((item) => item.fields)
+}
+
+export function attachLocPreviewDynamicFields(record = {}, enabledFields = []) {
+ const extendFields = record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
+ const dynamicValues = {}
+
+ enabledFields.forEach((field) => {
+ dynamicValues[getLocPreviewDynamicFieldKey(field.fields)] = extendFields[field.fields] || ''
+ })
+
+ return {
+ ...record,
+ ...dynamicValues
+ }
+}
+
+export function normalizeLocPreviewRow(record = {}) {
+ return {
+ ...record,
+ warehouseLabel: record['warehouseId$'] || record.warehouseName || '-',
+ areaLabel: record['areaId$'] || record.areaName || '-',
+ locCode: record.code || record.locCode || '-',
+ typeLabel: record['typeIds$'] || record['type$'] || record.type || '-',
+ useStatusLabel: record['useStatus$'] || '-',
+ barcode: record.barcode || '-',
+ row: normalizeNumber(record.row),
+ col: normalizeNumber(record.col),
+ lev: normalizeNumber(record.lev),
+ channel: normalizeNumber(record.channel),
+ statusLabel: record.statusBool ? '鍚敤' : '鍋滅敤',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-'
+ }
+}
+
+export function normalizeLocPreviewDetail(record = {}) {
+ return {
+ ...record,
+ warehouseLabel: record['warehouseId$'] || record.warehouseName || '-',
+ areaLabel: record['areaId$'] || record.areaName || '-',
+ locCode: record.code || record.locCode || '-',
+ typeLabel: record['typeIds$'] || record['type$'] || record.type || '-',
+ useStatusLabel: record['useStatus$'] || '-',
+ barcode: record.barcode || '-',
+ memo: record.memo || '-'
+ }
+}
+
+export function normalizeLocPreviewItemRow(record = {}, enabledFields = []) {
+ return attachLocPreviewDynamicFields(
+ {
+ ...record,
+ locCode: record.locCode || '-',
+ wareArea: record.wareArea || '-',
+ orderCode: record.orderCode || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ batch: record.batch || '-',
+ trackCode: record.trackCode || '-',
+ unit: record.unit || '-',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ workQty: normalizeNumber(record.workQty),
+ createTimeText: record['createTime$'] || record.createTime || '-'
+ },
+ enabledFields
+ )
+}
diff --git a/rsf-design/src/views/manager/loc-preview/locPreviewTable.columns.js b/rsf-design/src/views/manager/loc-preview/locPreviewTable.columns.js
new file mode 100644
index 0000000..7957a17
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-preview/locPreviewTable.columns.js
@@ -0,0 +1,77 @@
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createLocPreviewTableColumns({ handleViewDetail }) {
+ return [
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'warehouseLabel',
+ label: '浠撳簱',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'areaLabel',
+ label: '搴撳尯',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'typeLabel',
+ label: '搴撲綅绫诲瀷',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'barcode',
+ label: '鏉$爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'useStatusLabel',
+ label: '浣跨敤鐘舵��',
+ width: 120
+ },
+ {
+ prop: 'row',
+ label: '鎺�',
+ width: 80
+ },
+ {
+ prop: 'col',
+ label: '鍒�',
+ width: 80
+ },
+ {
+ prop: 'lev',
+ label: '灞�',
+ width: 80
+ },
+ {
+ prop: 'channel',
+ label: '宸烽亾',
+ width: 90
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'action',
+ label: '璇︽儏',
+ width: 100,
+ fixed: 'right',
+ useSlot: true,
+ align: 'center'
+ }
+ ]
+}
+
+export { ArtButtonTable }
diff --git a/rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue b/rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue
new file mode 100644
index 0000000..b9c3a65
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-preview/modules/loc-preview-detail-drawer.vue
@@ -0,0 +1,52 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撲綅璇︽儏"
+ size="85%"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱">{{ detail.warehouseLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯">{{ detail.areaLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤鐘舵��">{{ detail.useStatusLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅绫诲瀷">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉$爜">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎺�">{{ detail.row ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒�">{{ detail.col ?? '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="flex items-center justify-between">
+ <div class="text-sm text-[var(--art-gray-600)]">搴撳瓨鏄庣粏</div>
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/loc-revise/index.vue b/rsf-design/src/views/manager/loc-revise/index.vue
new file mode 100644
index 0000000..cea35c7
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-revise/index.vue
@@ -0,0 +1,542 @@
+<template>
+ <div class="loc-revise-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板搴撳瓨璋冩暣</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <span v-auth="'list'" class="inline-flex">
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </span>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <LocReviseDialog
+ v-model:visible="dialogVisible"
+ :loc-revise-data="currentLocReviseData"
+ :area-options="areaOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <LocReviseDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :summary="detailData"
+ :log-loading="logLoading"
+ :log-data="logTableData"
+ :log-columns="logColumns"
+ :log-pagination="logPagination"
+ :item-loading="itemLoading"
+ :item-data="itemTableData"
+ :item-columns="itemColumns"
+ :item-pagination="itemPagination"
+ :active-log="activeLog"
+ @log-size-change="handleLogSizeChange"
+ @log-current-change="handleLogCurrentChange"
+ @item-size-change="handleItemSizeChange"
+ @item-current-change="handleItemCurrentChange"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, h, onMounted, reactive, ref } from 'vue'
+ import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+ import {
+ fetchCompleteLocRevise,
+ fetchExportLocReviseReport,
+ fetchGetLocReviseDetail,
+ fetchGetLocReviseMany,
+ fetchLocRevisePage,
+ fetchReviseLogItemPage,
+ fetchReviseLogPage,
+ fetchSaveLocRevise,
+ fetchUpdateLocRevise,
+ fetchDeleteLocRevise,
+ fetchWarehouseAreasList
+ } from '@/api/loc-revise'
+ import LocReviseDialog from './modules/loc-revise-dialog.vue'
+ import LocReviseDetailDrawer from './modules/loc-revise-detail-drawer.vue'
+ import { createLocReviseTableColumns } from './locReviseTable.columns'
+ import {
+ buildLocReviseDialogModel,
+ buildLocRevisePageQueryParams,
+ buildLocRevisePrintRows,
+ buildLocReviseReportMeta,
+ buildLocReviseSavePayload,
+ buildLocReviseSearchParams,
+ buildReviseLogItemPageQueryParams,
+ buildReviseLogPageQueryParams,
+ createLocReviseFormState,
+ createLocReviseSearchState,
+ getLocReviseExceStatusOptions,
+ getLocReviseTypeOptions,
+ LOC_REVISE_REPORT_STYLE,
+ LOC_REVISE_REPORT_TITLE,
+ normalizeLocReviseRow,
+ normalizeReviseLogItemRow,
+ normalizeReviseLogRow,
+ resolveWarehouseAreaOptions
+ } from './locRevisePage.helpers'
+
+ defineOptions({ name: 'LocRevise' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+ const reportTitle = LOC_REVISE_REPORT_TITLE
+ const loading = ref(false)
+ const tableData = ref([])
+ const searchForm = ref(createLocReviseSearchState())
+ const areaOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const logLoading = ref(false)
+ const logTableData = ref([])
+ const itemLoading = ref(false)
+ const itemTableData = ref([])
+ const activeLog = ref({})
+ let handleDeleteAction = null
+
+ const pagination = reactive({ current: 1, size: 20, total: 0 })
+ const logPagination = reactive({ current: 1, size: 20, total: 0 })
+ const itemPagination = reactive({ current: 1, size: 20, total: 0 })
+ const reportQueryParams = computed(() => buildLocReviseSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヨ皟鏁村崟鍙�' }
+ },
+ {
+ label: '璋冩暣鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヨ皟鏁村崟鍙�' }
+ },
+ {
+ label: '璋冩暣绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: { clearable: true, options: getLocReviseTypeOptions() }
+ },
+ {
+ label: '搴撳尯鍚嶇О',
+ key: 'areaName',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ簱鍖哄悕绉�' }
+ },
+ {
+ label: '鎵ц鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getLocReviseExceStatusOptions().map((item) => ({
+ label: item.label,
+ value: item.value
+ }))
+ }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: { type: 'date', valueFormat: 'YYYY-MM-DD', placeholder: '璇烽�夋嫨寮�濮嬫椂闂�' }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: { type: 'date', valueFormat: 'YYYY-MM-DD', placeholder: '璇烽�夋嫨缁撴潫鏃堕棿' }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ activeLog.value = {}
+ itemTableData.value = []
+ try {
+ detailData.value = normalizeLocReviseRow(await fetchGetLocReviseDetail(row.id))
+ logPagination.current = 1
+ await loadLogData(row.id)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撳瓨璋冩暣璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentLocReviseData.value = buildLocReviseDialogModel(await fetchGetLocReviseDetail(row.id))
+ dialogVisible.value = true
+ dialogType.value = 'edit'
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇搴撳瓨璋冩暣璇︽儏澶辫触')
+ }
+ }
+
+ async function handleComplete(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸畬鎴愬簱瀛樿皟鏁村崟銆�${row.code || row.id}銆嶅悧锛焋, '瀹屾垚纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchCompleteLocRevise(row.id)
+ await loadPageData()
+ if (detailDrawerVisible.value && Number(detailData.value?.id) === Number(row.id)) {
+ await openDetail(row)
+ }
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '瀹屾垚搴撳瓨璋冩暣澶辫触')
+ }
+ }
+ }
+
+ const { columns, columnChecks } = useTableColumns(() =>
+ createLocReviseTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? (row) => openEditDialog(row) : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ handleComplete: hasAuth('update') ? (row) => handleComplete(row) : null
+ })
+ )
+
+ const logColumns = computed(() => [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'reviseCode', label: '璋冩暣鍗曞彿', minWidth: 170, showOverflowTooltip: true },
+ { prop: 'locCode', label: '搴撲綅缂栫爜', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'barcode', label: '搴撲綅鏉$爜', minWidth: 150, showOverflowTooltip: true },
+ { prop: 'typeLabel', label: '搴撲綅绫诲瀷', minWidth: 110 },
+ { prop: 'useStatusLabel', label: '鍗犵敤鐘舵��', minWidth: 110 },
+ { prop: 'updateTimeText', label: '鏇存柊鏃堕棿', minWidth: 170, showOverflowTooltip: true },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ text: '鏌ョ湅鏄庣粏',
+ onClick: () => openLogItems(row)
+ })
+ }
+ ])
+
+ const itemColumns = computed(() => [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'locCode', label: '搴撲綅缂栫爜', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'matnrCode', label: '鐗╂枡缂栫爜', minWidth: 150, showOverflowTooltip: true },
+ { prop: 'maktx', label: '鐗╂枡鍚嶇О', minWidth: 220, showOverflowTooltip: true },
+ { prop: 'unit', label: '鍗曚綅', width: 90 },
+ { prop: 'anfme', label: '鍘熷簱瀛�', width: 100, align: 'right' },
+ { prop: 'reviseQty', label: '璋冩暣鏁伴噺', width: 100, align: 'right' },
+ {
+ prop: 'diffQty',
+ label: '宸紓鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: Number(row.diffQty) === 0 ? 'info' : Number(row.diffQty) > 0 ? 'success' : 'danger',
+ effect: 'light'
+ },
+ () => String(row.diffQty)
+ )
+ },
+ { prop: 'batch', label: '鎵规', minWidth: 130, showOverflowTooltip: true },
+ { prop: 'spec', label: '瑙勬牸', minWidth: 130, showOverflowTooltip: true },
+ { prop: 'model', label: '鍨嬪彿', minWidth: 130, showOverflowTooltip: true }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadAreaOptions() {
+ const records = await guardRequestWithMessage(fetchWarehouseAreasList(), [], {
+ timeoutMessage: '搴撳尯閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ areaOptions.value = resolveWarehouseAreaOptions(records)
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchLocRevisePage(
+ buildLocRevisePageQueryParams({
+ ...searchForm.value,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ { records: [], total: 0, current: pagination.current, size: pagination.size },
+ { timeoutMessage: '搴撳瓨璋冩暣鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeLocReviseRow(record))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function loadLogData(reviseId = detailData.value?.id) {
+ if (!reviseId) return
+ logLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchReviseLogPage(
+ buildReviseLogPageQueryParams({
+ reviseId,
+ current: logPagination.current,
+ pageSize: logPagination.size
+ })
+ ),
+ { records: [], total: 0, current: logPagination.current, size: logPagination.size },
+ { timeoutMessage: '璋冩暣鏃ュ織鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ logTableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeReviseLogRow(record))
+ : []
+ updatePaginationState(logPagination, response, logPagination.current, logPagination.size)
+
+ if (logTableData.value.length > 0) {
+ const nextActiveLog = activeLog.value?.id
+ ? logTableData.value.find((item) => Number(item.id) === Number(activeLog.value.id))
+ : logTableData.value[0]
+ if (nextActiveLog) {
+ await openLogItems(nextActiveLog, { silent: true })
+ }
+ } else {
+ activeLog.value = {}
+ itemTableData.value = []
+ itemPagination.total = 0
+ }
+ } finally {
+ logLoading.value = false
+ }
+ }
+
+ async function openLogItems(row, options = {}) {
+ activeLog.value = row || {}
+ itemPagination.current = 1
+ await loadLogItemsData(options)
+ }
+
+ async function loadLogItemsData(options = {}) {
+ if (!activeLog.value?.id) {
+ itemTableData.value = []
+ return
+ }
+ itemLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchReviseLogItemPage(
+ buildReviseLogItemPageQueryParams({
+ reviseLogId: activeLog.value.id,
+ current: itemPagination.current,
+ pageSize: itemPagination.size
+ })
+ ),
+ { records: [], total: 0, current: itemPagination.current, size: itemPagination.size },
+ { timeoutMessage: options.silent ? '' : '鏃ュ織鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ itemTableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeReviseLogItemRow(record))
+ : []
+ updatePaginationState(itemPagination, response, itemPagination.current, itemPagination.size)
+ } finally {
+ itemLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ searchForm.value = createLocReviseSearchState()
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ function handleLogSizeChange(size) {
+ logPagination.size = size
+ logPagination.current = 1
+ loadLogData()
+ }
+
+ function handleLogCurrentChange(current) {
+ logPagination.current = current
+ loadLogData()
+ }
+
+ function handleItemSizeChange(size) {
+ itemPagination.size = size
+ itemPagination.current = 1
+ loadLogItemsData()
+ }
+
+ function handleItemCurrentChange(current) {
+ itemPagination.current = current
+ loadLogItemsData()
+ }
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentLocReviseData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => createLocReviseFormState(),
+ buildEditModel: (record) => buildLocReviseDialogModel(record),
+ buildSavePayload: (formData) => buildLocReviseSavePayload(formData),
+ saveRequest: fetchSaveLocRevise,
+ updateRequest: fetchUpdateLocRevise,
+ deleteRequest: fetchDeleteLocRevise,
+ entityName: '搴撳瓨璋冩暣',
+ resolveRecordLabel: (record) => record?.code || record?.id,
+ refreshCreate: loadPageData,
+ refreshUpdate: loadPageData,
+ refreshRemove: loadPageData
+ })
+ handleDeleteAction = handleDelete
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'loc-revise.xlsx',
+ requestExport: (payload) =>
+ fetchExportLocReviseReport(payload, {
+ headers: { Authorization: userStore.accessToken || '' }
+ }),
+ resolvePrintRecords: async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetLocReviseMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchLocRevisePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ },
+ buildPreviewRows: (records) => buildLocRevisePrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...LOC_REVISE_REPORT_STYLE }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildLocReviseReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || LOC_REVISE_REPORT_STYLE.orientation
+ })
+ )
+
+ onMounted(async () => {
+ await loadAreaOptions()
+ await loadPageData()
+ })
+</script>
diff --git a/rsf-design/src/views/manager/loc-revise/locRevisePage.helpers.js b/rsf-design/src/views/manager/loc-revise/locRevisePage.helpers.js
new file mode 100644
index 0000000..952cf2b
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-revise/locRevisePage.helpers.js
@@ -0,0 +1,233 @@
+export const LOC_REVISE_REPORT_TITLE = '搴撳瓨璋冩暣鎶ヨ〃'
+export const LOC_REVISE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const TYPE_OPTIONS = [
+ { label: '搴撳瓨璋冩暣', value: 0 },
+ { label: '鐩樼偣璋冩暣', value: 1 },
+ { label: '鍏跺畠璋冩暣', value: 2 }
+]
+
+const EXCE_STATUS_OPTIONS = [
+ { label: '鏈墽琛�', value: 0, tagType: 'info' },
+ { label: '鎵ц涓�', value: 1, tagType: 'warning' },
+ { label: '鎵ц瀹屾垚', value: 2, tagType: 'success' }
+]
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+export function getLocReviseTypeOptions() {
+ return TYPE_OPTIONS
+}
+
+export function getLocReviseExceStatusOptions() {
+ return EXCE_STATUS_OPTIONS
+}
+
+export function createLocReviseSearchState() {
+ return {
+ condition: '',
+ code: '',
+ type: '',
+ areaName: '',
+ exceStatus: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createLocReviseFormState() {
+ return {
+ id: null,
+ code: '',
+ type: 0,
+ areaId: '',
+ exceTime: '',
+ memo: '',
+ status: 1
+ }
+}
+
+export function buildLocReviseDialogModel(record = {}) {
+ const model = createLocReviseFormState()
+ return {
+ ...model,
+ ...record,
+ id: record.id ?? null,
+ code: record.code || '',
+ type: record.type ?? 0,
+ areaId: record.areaId ?? '',
+ exceTime: record.exceTime || '',
+ memo: record.memo || '',
+ status: record.status ?? 1
+ }
+}
+
+export function buildLocReviseSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'areaName', 'timeStart', 'timeEnd'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.type !== '' && params.type !== null && params.type !== undefined) {
+ result.type = normalizeNumber(params.type)
+ }
+ if (params.exceStatus !== '' && params.exceStatus !== null && params.exceStatus !== undefined) {
+ result.exceStatus = normalizeNumber(params.exceStatus)
+ }
+
+ return result
+}
+
+export function buildLocRevisePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildLocReviseSearchParams(params)
+ }
+}
+
+export function buildReviseLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.reviseId !== undefined ? { reviseId: params.reviseId } : {})
+ }
+}
+
+export function buildReviseLogItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.reviseLogId !== undefined ? { reviseLogId: params.reviseLogId } : {})
+ }
+}
+
+export function buildLocReviseSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: formData.id } : {}),
+ ...(formData.code ? { code: formData.code } : {}),
+ type: normalizeNumber(formData.type),
+ areaId: normalizeNumber(formData.areaId, null),
+ ...(normalizeText(formData.exceTime) ? { exceTime: normalizeText(formData.exceTime) } : {}),
+ ...(normalizeText(formData.memo) ? { memo: normalizeText(formData.memo) } : {}),
+ status: formData.status === 0 ? 0 : 1
+ }
+}
+
+export function resolveWarehouseAreaOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => ({
+ label: record.name || record.code || `搴撳尯${record.id}`,
+ value: record.id
+ }))
+}
+
+function resolveOptionLabel(options, value, fallback = '-') {
+ const target = options.find((item) => Number(item.value) === Number(value))
+ return target?.label || fallback
+}
+
+export function getLocReviseExceStatusMeta(value) {
+ const target = EXCE_STATUS_OPTIONS.find((item) => Number(item.value) === Number(value))
+ return target || { label: '-', value, tagType: 'info' }
+}
+
+export function normalizeLocReviseRow(record = {}) {
+ const exceStatusMeta = getLocReviseExceStatusMeta(record.exceStatus)
+ return {
+ ...record,
+ code: record.code || '-',
+ typeLabel: record['type$'] || resolveOptionLabel(TYPE_OPTIONS, record.type),
+ areaLabel: record.areaName || record['areaId$'] || '-',
+ anfme: normalizeNumber(record.anfme),
+ reviseQty: normalizeNumber(record.reviseQty),
+ exceStatusText: record['exceStatus$'] || exceStatusMeta.label,
+ exceStatusTagType: exceStatusMeta.tagType,
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createByText: record['createBy$'] || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ exceTimeText: record['exceTime$'] || record.exceTime || '-',
+ memo: record.memo || '-'
+ }
+}
+
+export function normalizeReviseLogRow(record = {}) {
+ return {
+ ...record,
+ reviseCode: record.reviseCode || '-',
+ locCode: record.locCode || record.barcode || '-',
+ barcode: record.barcode || '-',
+ typeLabel: record['type$'] || '-',
+ useStatusLabel: record['useStatus$'] || record.useStatus || '-',
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-'
+ }
+}
+
+export function normalizeReviseLogItemRow(record = {}) {
+ const diffQty = normalizeNumber(
+ record.diffQty,
+ normalizeNumber(record.reviseQty) - normalizeNumber(record.anfme)
+ )
+ return {
+ ...record,
+ locCode: record.locCode || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ unit: record.unit || '-',
+ anfme: normalizeNumber(record.anfme),
+ reviseQty: normalizeNumber(record.reviseQty),
+ diffQty,
+ batch: record.batch || '-',
+ spec: record.spec || '-',
+ model: record.model || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-'
+ }
+}
+
+export function buildLocRevisePrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeLocReviseRow(record))
+}
+
+export function buildLocReviseReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = LOC_REVISE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: LOC_REVISE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...LOC_REVISE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/manager/loc-revise/locReviseTable.columns.js b/rsf-design/src/views/manager/loc-revise/locReviseTable.columns.js
new file mode 100644
index 0000000..3b4f501
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-revise/locReviseTable.columns.js
@@ -0,0 +1,114 @@
+import { h } from 'vue'
+import { ElButton, ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createLocReviseTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ handleComplete
+}) {
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'code',
+ label: '璋冩暣鍗曞彿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'typeLabel',
+ label: '璋冩暣绫诲瀷',
+ minWidth: 120
+ },
+ {
+ prop: 'areaLabel',
+ label: '搴撳尯鍚嶇О',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '鍘熷簱瀛�',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'reviseQty',
+ label: '璋冩暣鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鎵ц鐘舵��',
+ width: 110,
+ formatter: (row) =>
+ h(ElTag, { type: row.exceStatusTagType || 'info', effect: 'light' }, () => row.exceStatusText)
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 110,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ minWidth: 220,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) =>
+ h(
+ 'div',
+ { class: 'flex justify-end gap-1' },
+ [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ row.exceStatus < 2 && handleEdit
+ ? h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ : null,
+ row.exceStatus === 0 && handleDelete
+ ? h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ : null,
+ row.exceStatus === 1 && handleComplete
+ ? h(
+ ElButton,
+ {
+ type: 'success',
+ link: true,
+ class: '!px-1',
+ onClick: () => handleComplete(row)
+ },
+ () => '瀹屾垚'
+ )
+ : null
+ ].filter(Boolean)
+ )
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/loc-revise/modules/loc-revise-detail-drawer.vue b/rsf-design/src/views/manager/loc-revise/modules/loc-revise-detail-drawer.vue
new file mode 100644
index 0000000..75b6c6d
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-revise/modules/loc-revise-detail-drawer.vue
@@ -0,0 +1,92 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳瓨璋冩暣璇︽儏"
+ size="78%"
+ destroy-on-close
+ @update:model-value="emit('update:visible', $event)"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElSkeleton :loading="loading" animated :rows="8">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="璋冩暣鍗曞彿">{{ summary.code || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璋冩暣绫诲瀷">{{ summary.typeLabel || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯鍚嶇О">{{ summary.areaLabel || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍘熷簱瀛�">{{ summary.anfme ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璋冩暣鏁伴噺">{{ summary.reviseQty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鐘舵��">
+ <ElTag :type="summary.exceStatusTagType || 'info'" effect="light">
+ {{ summary.exceStatusText || '-' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏃堕棿">{{ summary.exceTimeText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ summary.updateTimeText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ summary.memo || '-' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+
+ <div class="flex min-h-0 flex-1 flex-col gap-4">
+ <section class="flex min-h-0 flex-[1.1] flex-col gap-3">
+ <div class="flex items-center justify-between">
+ <h3 class="text-base font-semibold text-g-900">璋冩暣鏃ュ織</h3>
+ <span class="text-sm text-g-500">鍏� {{ logPagination.total || 0 }} 鏉�</span>
+ </div>
+ <ArtTable
+ :loading="logLoading"
+ :data="logData"
+ :columns="logColumns"
+ :pagination="logPagination"
+ @pagination:size-change="emit('log-size-change', $event)"
+ @pagination:current-change="emit('log-current-change', $event)"
+ />
+ </section>
+
+ <section class="flex min-h-0 flex-1 flex-col gap-3">
+ <div class="flex items-center justify-between">
+ <h3 class="text-base font-semibold text-g-900">鏃ュ織鏄庣粏</h3>
+ <span class="text-sm text-g-500">{{ activeLogLabel }}</span>
+ </div>
+ <ArtTable
+ :loading="itemLoading"
+ :data="itemData"
+ :columns="itemColumns"
+ :pagination="itemPagination"
+ @pagination:size-change="emit('item-size-change', $event)"
+ @pagination:current-change="emit('item-current-change', $event)"
+ />
+ </section>
+ </div>
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'LocReviseDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ summary: { type: Object, default: () => ({}) },
+ logLoading: { type: Boolean, default: false },
+ logData: { type: Array, default: () => [] },
+ logColumns: { type: Array, default: () => [] },
+ logPagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) },
+ itemLoading: { type: Boolean, default: false },
+ itemData: { type: Array, default: () => [] },
+ itemColumns: { type: Array, default: () => [] },
+ itemPagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) },
+ activeLog: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits([
+ 'update:visible',
+ 'log-size-change',
+ 'log-current-change',
+ 'item-size-change',
+ 'item-current-change'
+ ])
+
+ const activeLogLabel = computed(() =>
+ props.activeLog?.locCode ? `褰撳墠搴撲綅锛�${props.activeLog.locCode}` : '璇烽�夋嫨涓�鏉¤皟鏁存棩蹇�'
+ )
+</script>
diff --git a/rsf-design/src/views/manager/loc-revise/modules/loc-revise-dialog.vue b/rsf-design/src/views/manager/loc-revise/modules/loc-revise-dialog.vue
new file mode 100644
index 0000000..ca96962
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-revise/modules/loc-revise-dialog.vue
@@ -0,0 +1,168 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="760px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildLocReviseDialogModel,
+ createLocReviseFormState,
+ getLocReviseTypeOptions
+ } from '../locRevisePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ locReviseData: { type: Object, default: () => ({}) },
+ areaOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createLocReviseFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫搴撳瓨璋冩暣' : '鏂板搴撳瓨璋冩暣'))
+
+ const rules = computed(() => ({
+ type: [{ required: true, message: '璇烽�夋嫨璋冩暣绫诲瀷', trigger: 'change' }],
+ areaId: [{ required: true, message: '璇烽�夋嫨搴撳尯', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '璋冩暣鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ disabled: true,
+ placeholder: '鏂板鍚庤嚜鍔ㄧ敓鎴�'
+ }
+ },
+ {
+ label: '璋冩暣绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ options: getLocReviseTypeOptions(),
+ placeholder: '璇烽�夋嫨璋冩暣绫诲瀷'
+ }
+ },
+ {
+ label: '搴撳尯',
+ key: 'areaId',
+ type: 'select',
+ props: {
+ options: props.areaOptions,
+ placeholder: '璇烽�夋嫨搴撳尯',
+ filterable: true
+ }
+ },
+ {
+ label: '鎵ц鏃堕棿',
+ key: 'exceTime',
+ type: 'date',
+ props: {
+ type: 'datetime',
+ placeholder: '璇烽�夋嫨鎵ц鏃堕棿',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 4,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createLocReviseFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildLocReviseDialogModel(props.locReviseData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.locReviseData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/manager/menu-pda/index.vue b/rsf-design/src/views/manager/menu-pda/index.vue
new file mode 100644
index 0000000..d40a08f
--- /dev/null
+++ b/rsf-design/src/views/manager/menu-pda/index.vue
@@ -0,0 +1,222 @@
+<template>
+ <div class="menu-pda-page art-full-height">
+ <ArtSearchBar
+ v-model="formFilters"
+ :items="formItems"
+ :showExpand="false"
+ @reset="handleReset"
+ @search="handleSearch"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader
+ :showZebra="false"
+ :loading="loading"
+ v-model:columns="columnChecks"
+ @refresh="handleRefresh"
+ >
+ <template #left>
+ <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple>娣诲姞PDA鑿滃崟</ElButton>
+ <ElButton @click="toggleExpand" v-ripple>
+ {{ isExpanded ? '鏀惰捣' : '灞曞紑' }}
+ </ElButton>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ ref="tableRef"
+ rowKey="id"
+ :loading="loading"
+ :columns="columns"
+ :data="filteredTableData"
+ :stripe="false"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ :default-expand-all="false"
+ />
+
+ <MenuPdaDialog
+ v-model:visible="dialogVisible"
+ :editData="editData"
+ :lockType="lockMenuType"
+ :menuTreeOptions="menuTreeOptions"
+ @submit="handleSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import {
+ fetchDeleteMenuPda,
+ fetchGetMenuPdaTree,
+ fetchSaveMenuPda,
+ fetchUpdateMenuPda
+ } from '@/api/system-manage'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import MenuPdaDialog from './modules/menu-pda-dialog.vue'
+ import { createMenuPdaTableColumns } from './menuPdaTable.columns'
+ import {
+ buildMenuPdaSubmitPayload,
+ buildMenuPdaTreeOptions,
+ createMenuPdaSearchState,
+ filterMenuPdaTree,
+ getMenuPdaDisplayTitle
+ } from './menuPdaPage.helpers'
+
+ defineOptions({ name: 'MenuPda' })
+
+ const loading = ref(false)
+ const isExpanded = ref(false)
+ const tableRef = ref()
+ const dialogVisible = ref(false)
+ const editData = ref(null)
+ const lockMenuType = ref(true)
+ const tableData = ref([])
+ const menuTreeOptions = ref([])
+
+ const initialSearchState = createMenuPdaSearchState()
+ const formFilters = reactive({ ...initialSearchState })
+ const appliedFilters = reactive({ ...initialSearchState })
+
+ const formItems = computed(() => [
+ {
+ label: '鑿滃崟鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: { clearable: true }
+ },
+ {
+ label: '璺敱鍦板潃',
+ key: 'route',
+ type: 'input',
+ props: { clearable: true }
+ }
+ ])
+
+ const loadMenuPdaResources = async () => {
+ loading.value = true
+ try {
+ const list = await guardRequestWithMessage(fetchGetMenuPdaTree({ condition: appliedFilters.name || '' }), null, {
+ timeoutMessage: 'PDA鑿滃崟鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ if (list === null) {
+ tableData.value = []
+ menuTreeOptions.value = []
+ return
+ }
+ tableData.value = Array.isArray(list) ? list : []
+ menuTreeOptions.value = buildMenuPdaTreeOptions(tableData.value)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇PDA鑿滃崟澶辫触')
+ } finally {
+ loading.value = false
+ }
+ }
+
+ onMounted(() => {
+ loadMenuPdaResources()
+ })
+
+ const { columnChecks, columns } = useTableColumns(() =>
+ createMenuPdaTableColumns({
+ handleEditMenu,
+ handleDeleteMenu
+ })
+ )
+
+ const filteredTableData = computed(() => filterMenuPdaTree(tableData.value, appliedFilters))
+
+ function closeDialog() {
+ dialogVisible.value = false
+ editData.value = null
+ }
+
+ function handleAddMenu() {
+ editData.value = null
+ lockMenuType.value = false
+ dialogVisible.value = true
+ }
+
+ function handleEditMenu(row) {
+ editData.value = row
+ lockMenuType.value = true
+ dialogVisible.value = true
+ }
+
+ async function handleSubmit(formData) {
+ const payload = buildMenuPdaSubmitPayload(formData)
+ if (payload.id && payload.id === payload.parentId) {
+ ElMessage.error('涓婄骇鑿滃崟涓嶈兘閫夋嫨褰撳墠鑿滃崟')
+ return
+ }
+
+ try {
+ if (payload.id) {
+ await fetchUpdateMenuPda(payload)
+ ElMessage.success('淇敼鎴愬姛')
+ } else {
+ await fetchSaveMenuPda(payload)
+ ElMessage.success('鏂板鎴愬姛')
+ }
+ closeDialog()
+ await loadMenuPdaResources()
+ } catch (error) {
+ ElMessage.error(error?.message || '鎻愪氦澶辫触')
+ }
+ }
+
+ async function handleDeleteMenu(row) {
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄DA鑿滃崟銆�${getMenuPdaDisplayTitle(row)}銆嶅悧锛熷垹闄ゅ悗鏃犳硶鎭㈠`,
+ '鍒犻櫎纭',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ )
+ await fetchDeleteMenuPda(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await loadMenuPdaResources()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ function handleReset() {
+ Object.assign(formFilters, { ...initialSearchState })
+ Object.assign(appliedFilters, { ...initialSearchState })
+ loadMenuPdaResources()
+ }
+
+ function handleSearch() {
+ Object.assign(appliedFilters, { ...formFilters })
+ loadMenuPdaResources()
+ }
+
+ function handleRefresh() {
+ loadMenuPdaResources()
+ }
+
+ function toggleExpand() {
+ isExpanded.value = !isExpanded.value
+ nextTick(() => {
+ if (tableRef.value?.elTableRef && filteredTableData.value) {
+ const processRows = (rows) => {
+ rows.forEach((row) => {
+ if (row.children?.length) {
+ tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
+ processRows(row.children)
+ }
+ })
+ }
+ processRows(filteredTableData.value)
+ }
+ })
+ }
+</script>
diff --git a/rsf-design/src/views/manager/menu-pda/menuPdaPage.helpers.js b/rsf-design/src/views/manager/menu-pda/menuPdaPage.helpers.js
new file mode 100644
index 0000000..cc7786c
--- /dev/null
+++ b/rsf-design/src/views/manager/menu-pda/menuPdaPage.helpers.js
@@ -0,0 +1,124 @@
+export function createMenuPdaSearchState() {
+ return {
+ name: '',
+ route: ''
+ }
+}
+
+export function normalizeMenuPdaNumber(value, fallback = 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const normalized = Number(value)
+ return Number.isNaN(normalized) ? fallback : normalized
+}
+
+export function getMenuPdaDisplayTitle(row = {}) {
+ return String(row.name || '').trim()
+}
+
+export function getMenuPdaDisplayIcon(row = {}) {
+ return row.icon || ''
+}
+
+export function hasNestedMenuPda(row = {}) {
+ return Array.isArray(row.children) && row.children.some((child) => Number(child.type) !== 1)
+}
+
+export function getMenuPdaTypeTag(row = {}) {
+ if (Number(row.type) === 1) return 'danger'
+ if (hasNestedMenuPda(row)) return 'info'
+ return 'primary'
+}
+
+export function getMenuPdaTypeText(row = {}) {
+ if (Number(row.type) === 1) return '鎸夐挳'
+ if (hasNestedMenuPda(row)) return '鐩綍'
+ return '鑿滃崟'
+}
+
+export function getMenuPdaStatusMeta(status) {
+ return normalizeMenuPdaNumber(status, 1) === 1
+ ? { text: '鍚敤', type: 'success' }
+ : { text: '绂佺敤', type: 'danger' }
+}
+
+export function normalizeMenuPdaTreeOptions(nodes = []) {
+ if (!Array.isArray(nodes)) {
+ return []
+ }
+
+ return nodes.map((node) => ({
+ label: getMenuPdaDisplayTitle(node),
+ value: normalizeMenuPdaNumber(node.id, 0),
+ children: normalizeMenuPdaTreeOptions(node.children)
+ }))
+}
+
+export function buildMenuPdaTreeOptions(tree = []) {
+ return [
+ {
+ label: '椤剁骇鑿滃崟',
+ value: 0,
+ children: normalizeMenuPdaTreeOptions(tree)
+ }
+ ]
+}
+
+export function buildMenuPdaSubmitPayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: normalizeMenuPdaNumber(formData.id, 0) } : {}),
+ parentId: normalizeMenuPdaNumber(formData.parentId, 0),
+ name: String(formData.name || '').trim(),
+ route: String(formData.route || '').trim(),
+ component: String(formData.component || '').trim(),
+ authority: String(formData.authority || '').trim(),
+ icon: String(formData.icon || '').trim(),
+ sort: normalizeMenuPdaNumber(formData.sort, 0),
+ status: normalizeMenuPdaNumber(formData.status, 1),
+ memo: String(formData.memo || '').trim(),
+ type: formData.menuType === 'button' ? 1 : 0
+ }
+}
+
+export function cloneMenuPdaTree(source) {
+ if (source === null || typeof source !== 'object') return source
+ if (source instanceof Date) return new Date(source)
+ if (Array.isArray(source)) return source.map((item) => cloneMenuPdaTree(item))
+ const cloned = {}
+ for (const key in source) {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ cloned[key] = cloneMenuPdaTree(source[key])
+ }
+ }
+ return cloned
+}
+
+export function filterMenuPdaTree(items = [], filters = {}) {
+ const results = []
+ const searchName = String(filters.name || '').toLowerCase().trim()
+ const searchRoute = String(filters.route || '').toLowerCase().trim()
+
+ for (const item of items) {
+ const menuTitle = getMenuPdaDisplayTitle(item).toLowerCase()
+ const menuRoute = String(item.route || item.path || item.authority || '').toLowerCase()
+ const nameMatch = !searchName || menuTitle.includes(searchName)
+ const routeMatch = !searchRoute || menuRoute.includes(searchRoute)
+
+ if (item.children?.length) {
+ const matchedChildren = filterMenuPdaTree(item.children, filters)
+ if (matchedChildren.length > 0) {
+ const clonedItem = cloneMenuPdaTree(item)
+ clonedItem.children = matchedChildren
+ results.push(clonedItem)
+ continue
+ }
+ }
+
+ if (nameMatch && routeMatch) {
+ results.push(cloneMenuPdaTree(item))
+ }
+ }
+
+ return results
+}
diff --git a/rsf-design/src/views/manager/menu-pda/menuPdaTable.columns.js b/rsf-design/src/views/manager/menu-pda/menuPdaTable.columns.js
new file mode 100644
index 0000000..3f53904
--- /dev/null
+++ b/rsf-design/src/views/manager/menu-pda/menuPdaTable.columns.js
@@ -0,0 +1,97 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+import {
+ getMenuPdaDisplayIcon,
+ getMenuPdaDisplayTitle,
+ getMenuPdaStatusMeta,
+ getMenuPdaTypeTag,
+ getMenuPdaTypeText
+} from './menuPdaPage.helpers'
+
+export function createMenuPdaTableColumns({ handleEditMenu, handleDeleteMenu }) {
+ return [
+ {
+ prop: 'name',
+ label: '鑿滃崟鍚嶇О',
+ minWidth: 180,
+ formatter: (row) => getMenuPdaDisplayTitle(row)
+ },
+ {
+ prop: 'icon',
+ label: '鍥炬爣棰勮',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const icon = getMenuPdaDisplayIcon(row)
+ if (!icon) return h('span', { class: 'text-g-400' }, '-')
+ return h(
+ 'div',
+ {
+ class:
+ 'mx-auto flex h-8 w-8 items-center justify-center rounded-md border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)]'
+ },
+ [h(ArtSvgIcon, { icon, class: 'text-base text-g-700' })]
+ )
+ }
+ },
+ {
+ prop: 'type',
+ label: '鑿滃崟绫诲瀷',
+ width: 110,
+ formatter: (row) =>
+ h(ElTag, { type: getMenuPdaTypeTag(row), effect: 'light' }, () => getMenuPdaTypeText(row))
+ },
+ {
+ prop: 'route',
+ label: '璺敱',
+ minWidth: 180,
+ formatter: (row) => row.route || ''
+ },
+ {
+ prop: 'authority',
+ label: '鏉冮檺鏍囪瘑',
+ minWidth: 180,
+ formatter: (row) => row.authority || '-'
+ },
+ {
+ prop: 'sort',
+ label: '鎺掑簭',
+ width: 90
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => {
+ const statusMeta = getMenuPdaStatusMeta(row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 120,
+ align: 'right',
+ formatter: (row) =>
+ h('div', { class: 'flex justify-end' }, [
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEditMenu(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDeleteMenu(row)
+ })
+ ])
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/menu-pda/modules/menu-pda-dialog.vue b/rsf-design/src/views/manager/menu-pda/modules/menu-pda-dialog.vue
new file mode 100644
index 0000000..05186b8
--- /dev/null
+++ b/rsf-design/src/views/manager/menu-pda/modules/menu-pda-dialog.vue
@@ -0,0 +1,273 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ @update:model-value="handleCancel"
+ width="760px"
+ align-center
+ class="menu-dialog"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="24"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ >
+ <template #menuType>
+ <ElRadioGroup v-model="form.menuType" :disabled="disableMenuType">
+ <ElRadioButton value="menu">鑿滃崟</ElRadioButton>
+ <ElRadioButton value="button">鎸夐挳</ElRadioButton>
+ </ElRadioGroup>
+ </template>
+ </ArtForm>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+
+ const createMenuPdaFormState = () => ({
+ menuType: 'menu',
+ id: null,
+ parentId: 0,
+ name: '',
+ route: '',
+ component: '',
+ authority: '',
+ icon: '',
+ sort: 0,
+ status: 1,
+ memo: ''
+ })
+
+ const props = defineProps({
+ visible: { required: false, default: false },
+ lockType: { required: false, default: false },
+ editData: { required: false, default: null },
+ menuTreeOptions: { required: false, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createMenuPdaFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => `${isEdit.value ? '缂栬緫' : '鏂板缓'}${form.menuType === 'button' ? '鎸夐挳' : '鑿滃崟'}`)
+ const disableMenuType = computed(() => props.lockType || isEdit.value)
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: form.menuType === 'button' ? '璇疯緭鍏ユ潈闄愬悕绉�' : '璇疯緭鍏ヨ彍鍗曞悕绉�', trigger: 'blur' }],
+ route: form.menuType === 'menu' ? [{ required: true, message: '璇疯緭鍏ヨ矾鐢卞湴鍧�', trigger: 'blur' }] : [],
+ authority:
+ form.menuType === 'button'
+ ? [{ required: true, message: '璇疯緭鍏ユ潈闄愭爣璇�', trigger: 'blur' }]
+ : []
+ }))
+
+ const formItems = computed(() => {
+ const items = [
+ { label: '鑿滃崟绫诲瀷', key: 'menuType', span: 24 },
+ {
+ label: '涓婄骇鑿滃崟',
+ key: 'parentId',
+ type: 'treeselect',
+ span: 24,
+ props: {
+ data: props.menuTreeOptions,
+ props: {
+ label: 'label',
+ value: 'value',
+ children: 'children'
+ },
+ placeholder: '璇烽�夋嫨涓婄骇鑿滃崟',
+ checkStrictly: true,
+ clearable: false,
+ defaultExpandAll: true
+ }
+ },
+ {
+ label: form.menuType === 'button' ? '鏉冮檺鍚嶇О' : '鑿滃崟鍚嶇О',
+ key: 'name',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: form.menuType === 'button' ? '璇疯緭鍏ユ潈闄愬悕绉�' : '璇疯緭鍏ヨ彍鍗曞悕绉�',
+ clearable: true
+ }
+ }
+ ]
+
+ if (form.menuType === 'menu') {
+ items.push(
+ {
+ label: '璺敱鍦板潃',
+ key: 'route',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ヨ矾鐢卞湴鍧�',
+ clearable: true
+ }
+ },
+ {
+ label: '缁勪欢鏍囪瘑',
+ key: 'component',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ョ粍浠舵爣璇�',
+ clearable: true
+ }
+ }
+ )
+ }
+
+ items.push(
+ {
+ label: '鏉冮檺鏍囪瘑',
+ key: 'authority',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ユ潈闄愭爣璇�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍥炬爣',
+ key: 'icon',
+ type: 'input',
+ span: 24,
+ props: {
+ placeholder: '璇疯緭鍏ュ浘鏍囧悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '鎺掑簭',
+ key: 'sort',
+ type: 'number',
+ span: 24,
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ span: 24,
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: [
+ { label: '鍚敤', value: 1 },
+ { label: '绂佺敤', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ )
+
+ return items
+ })
+
+ const normalizeNumber = (value, fallback = 0) => {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const normalized = Number(value)
+ return Number.isNaN(normalized) ? fallback : normalized
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createMenuPdaFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const loadFormData = () => {
+ resetForm()
+ const row = props.editData
+ if (!row || typeof row !== 'object') {
+ return
+ }
+ form.menuType = Number(row.type) === 1 ? 'button' : 'menu'
+ form.id = row.id ?? null
+ form.parentId = normalizeNumber(row.parentId, 0)
+ form.name = row.name || ''
+ form.route = row.route || ''
+ form.component = row.component || ''
+ form.authority = row.authority || ''
+ form.icon = row.icon || ''
+ form.sort = normalizeNumber(row.sort, 0)
+ form.status = normalizeNumber(row.status, 1)
+ form.memo = row.memo || ''
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', {
+ ...form,
+ type: form.menuType === 'button' ? 1 : 0
+ })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.editData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/manager/qly-inspect/index.vue b/rsf-design/src/views/manager/qly-inspect/index.vue
new file mode 100644
index 0000000..405cad4
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-inspect/index.vue
@@ -0,0 +1,416 @@
+<template>
+ <div class="qly-inspect-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData">
+ <template #left>
+ <ElSpace wrap>
+ <span v-auth="'list'" class="inline-flex">
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </span>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <QlyInspectItemsDrawer
+ v-model:visible="itemsDrawerVisible"
+ :loading="itemsLoading"
+ :summary="activeRow"
+ :data="itemsTableData"
+ :columns="itemColumns"
+ :pagination="itemsPagination"
+ @size-change="handleItemsSizeChange"
+ @current-change="handleItemsCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import {
+ fetchExportQlyInspectReport,
+ fetchGetQlyInspectMany,
+ fetchQlyInspectItemPage,
+ fetchQlyInspectPage
+ } from '@/api/qly-inspect'
+ import QlyInspectItemsDrawer from './modules/qly-inspect-items-drawer.vue'
+ import { createQlyInspectTableColumns } from './qlyInspectTable.columns'
+ import {
+ buildQlyInspectItemsQueryParams,
+ buildQlyInspectPageQueryParams,
+ buildQlyInspectPrintRows,
+ buildQlyInspectReportMeta,
+ buildQlyInspectSearchParams,
+ createQlyInspectSearchState,
+ normalizeQlyInspectItemRow,
+ normalizeQlyInspectRow,
+ QLY_INSPECT_REPORT_STYLE,
+ QLY_INSPECT_REPORT_TITLE
+ } from './qlyInspectPage.helpers'
+
+ defineOptions({ name: 'QlyInspect' })
+
+ const userStore = useUserStore()
+ const reportTitle = QLY_INSPECT_REPORT_TITLE
+ const loading = ref(false)
+ const tableData = ref([])
+ const selectedRows = ref([])
+ const searchForm = ref(createQlyInspectSearchState())
+
+ const itemsDrawerVisible = ref(false)
+ const itemsLoading = ref(false)
+ const itemsTableData = ref([])
+ const activeRow = ref({})
+
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const itemsPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildQlyInspectSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ川妫�鍗曞彿/鏉ユ簮鍗曞彿'
+ }
+ },
+ {
+ label: '璐ㄦ鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ川妫�鍗曞彿'
+ }
+ },
+ {
+ label: '鍗曟嵁绫诲瀷',
+ key: 'wkType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟鎹被鍨�'
+ }
+ },
+ {
+ label: '鏉ユ簮鍗曞彿',
+ key: 'asnCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潵婧愬崟鍙�'
+ }
+ },
+ {
+ label: '璐ㄦ鐘舵��',
+ key: 'isptStatus',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ川妫�鐘舵��'
+ }
+ }
+ ])
+
+ const { columns, columnChecks } = useTableColumns(() =>
+ createQlyInspectTableColumns({
+ handleViewItems: openItemsDrawer
+ })
+ )
+
+ const itemColumns = computed(() => [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'ispectId',
+ label: '璐ㄦ鍗旾D',
+ minWidth: 110
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'label',
+ label: '鏍囩',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'stockBatch',
+ label: '搴撳瓨鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'rcptQty',
+ label: '鏀惰揣鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'dlyQty',
+ label: '閫佹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'anfme',
+ label: '纭鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'isptResultText',
+ label: '璐ㄦ缁撴灉',
+ minWidth: 120
+ }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchQlyInspectPage(
+ buildQlyInspectPageQueryParams({
+ ...searchForm.value,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: pagination.current,
+ size: pagination.size
+ },
+ {
+ timeoutMessage: '璐ㄦ淇℃伅鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeQlyInspectRow(record))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function loadItemsData() {
+ if (!activeRow.value?.id) {
+ return
+ }
+
+ itemsLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchQlyInspectItemPage(
+ buildQlyInspectItemsQueryParams({
+ ispectId: activeRow.value.id,
+ current: itemsPagination.current,
+ pageSize: itemsPagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: itemsPagination.current,
+ size: itemsPagination.size
+ },
+ {
+ timeoutMessage: '璐ㄦ淇℃伅鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ itemsTableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeQlyInspectItemRow(record))
+ : []
+ updatePaginationState(itemsPagination, response, itemsPagination.current, itemsPagination.size)
+ } finally {
+ itemsLoading.value = false
+ }
+ }
+
+ function openItemsDrawer(row) {
+ activeRow.value = row
+ itemsPagination.current = 1
+ itemsDrawerVisible.value = true
+ loadItemsData()
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ searchForm.value = createQlyInspectSearchState()
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ function handleItemsSizeChange(size) {
+ itemsPagination.size = size
+ itemsPagination.current = 1
+ loadItemsData()
+ }
+
+ function handleItemsCurrentChange(current) {
+ itemsPagination.current = current
+ loadItemsData()
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'qly-inspect.xlsx',
+ requestExport: (payload) =>
+ fetchExportQlyInspectReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords: async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetQlyInspectMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchQlyInspectPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ },
+ buildPreviewRows: (records) => buildQlyInspectPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...QLY_INSPECT_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildQlyInspectReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || QLY_INSPECT_REPORT_STYLE.orientation
+ })
+ )
+
+ onMounted(() => {
+ loadPageData()
+ })
+</script>
diff --git a/rsf-design/src/views/manager/qly-inspect/modules/qly-inspect-items-drawer.vue b/rsf-design/src/views/manager/qly-inspect/modules/qly-inspect-items-drawer.vue
new file mode 100644
index 0000000..99797ea
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-inspect/modules/qly-inspect-items-drawer.vue
@@ -0,0 +1,44 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="璐ㄦ淇℃伅鏄庣粏"
+ size="72%"
+ destroy-on-close
+ @update:model-value="emit('update:visible', $event)"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="璐ㄦ鍗曞彿">{{ summary.code || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ summary.wkTypeLabel || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉ユ簮鍗曞彿">{{ summary.asnCode || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐ㄦ鏁伴噺">{{ summary.isptQty ?? '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐ㄦ鐘舵��">{{ summary.isptStatusText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ summary.updateByText || '-' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="emit('size-change', $event)"
+ @pagination:current-change="emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'QlyInspectItemsDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ summary: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'size-change', 'current-change'])
+</script>
diff --git a/rsf-design/src/views/manager/qly-inspect/qlyInspectPage.helpers.js b/rsf-design/src/views/manager/qly-inspect/qlyInspectPage.helpers.js
new file mode 100644
index 0000000..432efc5
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-inspect/qlyInspectPage.helpers.js
@@ -0,0 +1,125 @@
+export const QLY_INSPECT_REPORT_TITLE = '璐ㄦ淇℃伅鎶ヨ〃'
+export const QLY_INSPECT_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+export function createQlyInspectSearchState() {
+ return {
+ condition: '',
+ code: '',
+ wkType: '',
+ asnCode: '',
+ isptStatus: '',
+ isptQty: ''
+ }
+}
+
+export function buildQlyInspectSearchParams(params = {}) {
+ const result = {}
+
+ ;['condition', 'code', 'wkType', 'asnCode', 'isptStatus', 'isptQty'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function buildQlyInspectPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildQlyInspectSearchParams(params)
+ }
+}
+
+export function buildQlyInspectItemsQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.ispectId !== undefined ? { ispectId: params.ispectId } : {})
+ }
+}
+
+export function normalizeQlyInspectRow(record = {}) {
+ return {
+ ...record,
+ code: record.code || '-',
+ wkTypeLabel: record['wkType$'] || record.wkType || '-',
+ asnId: record.asnId ?? '-',
+ asnCode: record.asnCode || '-',
+ isptQty: normalizeNumber(record.isptQty),
+ isptStatusText: record['isptStatus$'] || '-',
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createByText: record['createBy$'] || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ statusText:
+ record.statusBool === true || Number(record.status) === 1
+ ? '鍚敤'
+ : record.statusBool === false || Number(record.status) === 0
+ ? '鍋滅敤'
+ : '-',
+ memo: record.memo || '-'
+ }
+}
+
+export function normalizeQlyInspectItemRow(record = {}) {
+ return {
+ ...record,
+ ispectId: record.ispectId ?? '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ label: record.label || '-',
+ splrBatch: record.splrBatch || '-',
+ stockBatch: record.stockBatch || '-',
+ rcptQty: normalizeNumber(record.rcptQty),
+ dlyQty: normalizeNumber(record.dlyQty),
+ anfme: normalizeNumber(record.anfme),
+ splrName: record.splrName || '-',
+ isptResultText: record['isptResult$'] || '-'
+ }
+}
+
+export function buildQlyInspectPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeQlyInspectRow(record))
+}
+
+export function buildQlyInspectReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = QLY_INSPECT_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: QLY_INSPECT_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...QLY_INSPECT_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/manager/qly-inspect/qlyInspectTable.columns.js b/rsf-design/src/views/manager/qly-inspect/qlyInspectTable.columns.js
new file mode 100644
index 0000000..73354f2
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-inspect/qlyInspectTable.columns.js
@@ -0,0 +1,77 @@
+import { h } from 'vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createQlyInspectTableColumns({ handleViewItems }) {
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'code',
+ label: '璐ㄦ鍗曞彿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'asnId',
+ label: '鏉ユ簮鍗旾D',
+ minWidth: 110
+ },
+ {
+ prop: 'asnCode',
+ label: '鏉ユ簮鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'isptQty',
+ label: '璐ㄦ鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'isptStatusText',
+ label: '璐ㄦ鐘舵��',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 110,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ text: '鏌ョ湅鏄庣粏',
+ onClick: () => handleViewItems(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/qly-ispt-item-result/index.vue b/rsf-design/src/views/manager/qly-ispt-item-result/index.vue
new file mode 100644
index 0000000..9bedef1
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-ispt-item-result/index.vue
@@ -0,0 +1,74 @@
+<template>
+ <div class="qly-ispt-item-result-page art-full-height">
+ <ElCard class="art-table-card">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div>
+ <div class="text-base font-semibold text-gray-900">璐ㄦ缁撴灉鏄庣粏</div>
+ <div class="mt-1 text-sm text-gray-500">鎸夎川妫�鍗曠淮搴︽煡鐪嬭川妫�缁撴灉鏄庣粏</div>
+ </div>
+ <ElButton @click="refreshData" :loading="loading">鍒锋柊</ElButton>
+ </div>
+ </template>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <QlyIsptItemDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useRoute } from 'vue-router'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchGetQlyIsptItemDetail, fetchQlyIsptItemResultPage } from '@/api/qly-ispt-item'
+ import {
+ buildQlyIsptItemResultPageQueryParams,
+ getQlyIsptItemPaginationKey,
+ normalizeQlyIsptItemRow
+ } from '../qly-ispt-item/qlyIsptItemPage.helpers'
+ import { createQlyIsptItemTableColumns } from '../qly-ispt-item/qlyIsptItemTable.columns'
+ import QlyIsptItemDetailDrawer from '../qly-ispt-item/modules/qly-ispt-item-detail-drawer.vue'
+
+ defineOptions({ name: 'QlyIsptItemResult' })
+
+ const route = useRoute()
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+
+ const inspectId = computed(() => route.query.id || route.query.ispectId || '')
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const { columns, data, loading, pagination, handleSizeChange, handleCurrentChange, refreshData } = useTable({
+ core: {
+ apiFn: fetchQlyIsptItemResultPage,
+ apiParams: buildQlyIsptItemResultPageQueryParams({ id: inspectId.value }),
+ paginationKey: getQlyIsptItemPaginationKey(),
+ columnsFactory: () => createQlyIsptItemTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeQlyIsptItemRow(item)) : []
+ }
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetQlyIsptItemDetail(id)
+ detailData.value = normalizeQlyIsptItemRow({
+ ...fallback,
+ ...detail
+ })
+ }
+</script>
diff --git a/rsf-design/src/views/manager/qly-ispt-item/index.vue b/rsf-design/src/views/manager/qly-ispt-item/index.vue
new file mode 100644
index 0000000..c729b59
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-ispt-item/index.vue
@@ -0,0 +1,245 @@
+<template>
+ <div class="qly-ispt-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <QlyIsptItemDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useRoute } from 'vue-router'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ fetchExportQlyIsptItemReport,
+ fetchGetQlyIsptItemDetail,
+ fetchGetQlyIsptItemMany,
+ fetchQlyIsptItemPage
+ } from '@/api/qly-ispt-item'
+ import {
+ buildQlyIsptItemPageQueryParams,
+ buildQlyIsptItemPrintRows,
+ buildQlyIsptItemSearchParams,
+ createQlyIsptItemSearchState,
+ getQlyIsptItemPaginationKey,
+ getQlyIsptItemReportColumns,
+ normalizeQlyIsptItemRow,
+ QLY_ISPT_ITEM_REPORT_TITLE
+ } from './qlyIsptItemPage.helpers'
+ import { createQlyIsptItemTableColumns } from './qlyIsptItemTable.columns'
+ import QlyIsptItemDetailDrawer from './modules/qly-ispt-item-detail-drawer.vue'
+
+ defineOptions({ name: 'QlyIsptItem' })
+
+ const route = useRoute()
+ const userStore = useUserStore()
+ const initialInspectId = route.query.ispectId || route.query.id || ''
+ const searchForm = ref(createQlyIsptItemSearchState({ ispectId: initialInspectId }))
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = QLY_ISPT_ITEM_REPORT_TITLE
+ const reportQueryParams = computed(() => buildQlyIsptItemSearchParams(searchForm.value))
+ const reportColumns = getQlyIsptItemReportColumns()
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�/鍚嶇О/渚涘簲鍟�'
+ }
+ },
+ {
+ label: '璐ㄦ鍗旾D',
+ key: 'ispectId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ川妫�鍗旾D'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '渚涘簲鍟�',
+ key: 'splrName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢'
+ }
+ },
+ {
+ label: '瀹㈡埛璁㈠崟鍙�',
+ key: 'platOrderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鎴疯鍗曞彿'
+ }
+ },
+ {
+ label: '宸ュ崟鍙�',
+ key: 'platWorkCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ伐鍗曞彿'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchQlyIsptItemPage,
+ apiParams: buildQlyIsptItemPageQueryParams(searchForm.value),
+ paginationKey: getQlyIsptItemPaginationKey(),
+ columnsFactory: () => createQlyIsptItemTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeQlyIsptItemRow(item)) : []
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetQlyIsptItemMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchQlyIsptItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'qly-ispt-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportQlyIsptItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildQlyIsptItemPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetQlyIsptItemDetail(id)
+ detailData.value = normalizeQlyIsptItemRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildQlyIsptItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ const resetSeed = route.query.ispectId || route.query.id ? { ispectId: route.query.ispectId || route.query.id } : {}
+ Object.assign(searchForm.value, createQlyIsptItemSearchState(resetSeed))
+ resetSearchParams(buildQlyIsptItemPageQueryParams(createQlyIsptItemSearchState(resetSeed)))
+ }
+</script>
diff --git a/rsf-design/src/views/manager/qly-ispt-item/modules/qly-ispt-item-detail-drawer.vue b/rsf-design/src/views/manager/qly-ispt-item/modules/qly-ispt-item-detail-drawer.vue
new file mode 100644
index 0000000..868b7b5
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-ispt-item/modules/qly-ispt-item-detail-drawer.vue
@@ -0,0 +1,49 @@
+<template>
+ <ElDrawer :model-value="visible" title="璐ㄦ鏄庣粏璇︽儏" size="72%" @update:model-value="handleVisibleChange">
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="鏄庣粏ID">{{ detail.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐ㄦ鍗旾D">{{ detail.ispectId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏍囩">{{ detail.label ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟�">{{ detail.splrName ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鎵规">{{ detail.stockBatch ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛璁㈠崟鍙�">{{ detail.platOrderCode ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ュ崟鍙�">{{ detail.platWorkCode ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰鍙�">{{ detail.projectCode ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐ㄦ缁撴灉">{{ detail.isptResultText ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏀惰揣鏁伴噺">{{ detail.rcptQty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閫佹鏁伴噺">{{ detail.dlyQty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓嶅悎鏍兼暟閲�">{{ detail.disQty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚堟牸鏁伴噺">{{ detail.safeQty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="纭鏁伴噺">{{ detail.anfme ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem :span="2" label="澶囨敞">{{ detail.memo ?? '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'QlyIsptItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: {
+ type: Object,
+ default: () => ({})
+ }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/qly-ispt-item/qlyIsptItemPage.helpers.js b/rsf-design/src/views/manager/qly-ispt-item/qlyIsptItemPage.helpers.js
new file mode 100644
index 0000000..f149070
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-ispt-item/qlyIsptItemPage.helpers.js
@@ -0,0 +1,167 @@
+export const QLY_ISPT_ITEM_REPORT_TITLE = '璐ㄦ淇℃伅鏄庣粏鎶ヨ〃'
+
+export function createQlyIsptItemSearchState(seed = {}) {
+ return {
+ condition: '',
+ ispectId: '',
+ matnrCode: '',
+ maktx: '',
+ label: '',
+ splrName: '',
+ splrBatch: '',
+ stockBatch: '',
+ platOrderCode: '',
+ platWorkCode: '',
+ projectCode: '',
+ rcptQty: '',
+ dlyQty: '',
+ disQty: '',
+ safeQty: '',
+ picPath: '',
+ memo: '',
+ status: '',
+ ...seed
+ }
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return undefined
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : undefined
+}
+
+export function getQlyIsptItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildQlyIsptItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'matnrCode',
+ 'maktx',
+ 'label',
+ 'splrName',
+ 'splrBatch',
+ 'stockBatch',
+ 'platOrderCode',
+ 'platWorkCode',
+ 'projectCode',
+ 'picPath',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['ispectId', 'rcptQty', 'dlyQty', 'disQty', 'safeQty', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== undefined) {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function buildQlyIsptItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildQlyIsptItemSearchParams(params)
+ }
+}
+
+export function buildQlyIsptItemResultPageQueryParams(params = {}) {
+ const id = normalizeNumber(params.id ?? params.ispectId)
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(id !== undefined ? { id } : {})
+ }
+}
+
+function resolveStatusText(record) {
+ if (record.statusBool === true || Number(record.status) === 1) {
+ return '姝e父'
+ }
+ if (record.statusBool === false || Number(record.status) === 0) {
+ return '鍋滅敤'
+ }
+ return '--'
+}
+
+export function normalizeQlyIsptItemRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? '--',
+ ispectId: record.ispectId ?? '--',
+ matnrCode: record.matnrCode || '--',
+ maktx: record.maktx || '--',
+ label: record.label || '--',
+ splrName: record.splrName || '--',
+ splrBatch: record.splrBatch || '--',
+ stockBatch: record.stockBatch || '--',
+ platOrderCode: record.platOrderCode || '--',
+ platWorkCode: record.platWorkCode || '--',
+ projectCode: record.projectCode || '--',
+ rcptQty: record.rcptQty ?? 0,
+ dlyQty: record.dlyQty ?? 0,
+ disQty: record.disQty ?? 0,
+ safeQty: record.safeQty ?? 0,
+ anfme: record.anfme ?? 0,
+ isptResultText: record['isptResult$'] || '--',
+ statusText: resolveStatusText(record),
+ createByText: record['createBy$'] || '--',
+ createTimeText: record['createTime$'] || record.createTime || '--',
+ updateByText: record['updateBy$'] || '--',
+ updateTimeText: record['updateTime$'] || record.updateTime || '--',
+ memo: record.memo || '--'
+ }
+}
+
+export function getQlyIsptItemReportColumns() {
+ return [
+ { prop: 'ispectId', label: '璐ㄦ鍗旾D' },
+ { prop: 'matnrCode', label: '鐗╂枡缂栫爜' },
+ { prop: 'maktx', label: '鐗╂枡鍚嶇О' },
+ { prop: 'label', label: '鏍囩' },
+ { prop: 'splrName', label: '渚涘簲鍟�' },
+ { prop: 'splrBatch', label: '渚涘簲鍟嗘壒娆�' },
+ { prop: 'stockBatch', label: '搴撳瓨鎵规' },
+ { prop: 'platOrderCode', label: '瀹㈡埛璁㈠崟鍙�' },
+ { prop: 'platWorkCode', label: '宸ュ崟鍙�' },
+ { prop: 'projectCode', label: '椤圭洰鍙�' },
+ { prop: 'rcptQty', label: '鏀惰揣鏁伴噺' },
+ { prop: 'dlyQty', label: '閫佹鏁伴噺' },
+ { prop: 'disQty', label: '涓嶅悎鏍兼暟閲�' },
+ { prop: 'safeQty', label: '鍚堟牸鏁伴噺' },
+ { prop: 'anfme', label: '纭鏁伴噺' },
+ { prop: 'isptResultText', label: '璐ㄦ缁撴灉' },
+ { prop: 'createByText', label: '鍒涘缓浜�' },
+ { prop: 'createTimeText', label: '鍒涘缓鏃堕棿' },
+ { prop: 'updateByText', label: '鏇存柊浜�' },
+ { prop: 'updateTimeText', label: '鏇存柊鏃堕棿' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildQlyIsptItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeQlyIsptItemRow(record))
+}
diff --git a/rsf-design/src/views/manager/qly-ispt-item/qlyIsptItemTable.columns.js b/rsf-design/src/views/manager/qly-ispt-item/qlyIsptItemTable.columns.js
new file mode 100644
index 0000000..a2184c3
--- /dev/null
+++ b/rsf-design/src/views/manager/qly-ispt-item/qlyIsptItemTable.columns.js
@@ -0,0 +1,150 @@
+import { h } from 'vue'
+import { ElButton, ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createQlyIsptItemTableColumns({ handleView }) {
+ return [
+ {
+ type: 'selection',
+ width: 52,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'ispectId',
+ label: '璐ㄦ鍗旾D',
+ minWidth: 110
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'label',
+ label: '鏍囩',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'stockBatch',
+ label: '搴撳瓨鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platOrderCode',
+ label: '瀹㈡埛璁㈠崟鍙�',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platWorkCode',
+ label: '宸ュ崟鍙�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'projectCode',
+ label: '椤圭洰鍙�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'rcptQty',
+ label: '鏀惰揣鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'dlyQty',
+ label: '閫佹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'disQty',
+ label: '涓嶅悎鏍兼暟閲�',
+ width: 120,
+ align: 'right'
+ },
+ {
+ prop: 'safeQty',
+ label: '鍚堟牸鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'anfme',
+ label: '纭鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'isptResultText',
+ label: '璐ㄦ缁撴灉',
+ minWidth: 120
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: row.statusText === '姝e父' ? 'success' : 'info' },
+ () => row.statusText || '--'
+ )
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170
+ },
+ {
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(
+ ArtButtonTable,
+ {},
+ () =>
+ h(
+ ElButton,
+ {
+ link: true,
+ type: 'primary',
+ onClick: () => handleView(row)
+ },
+ () => '璇︽儏'
+ )
+ )
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/revise-log-item/index.vue b/rsf-design/src/views/manager/revise-log-item/index.vue
new file mode 100644
index 0000000..bd361b2
--- /dev/null
+++ b/rsf-design/src/views/manager/revise-log-item/index.vue
@@ -0,0 +1,174 @@
+<template>
+ <div class="revise-log-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <ReviseLogItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchGetReviseLogItemDetail, fetchReviseLogItemPage } from '@/api/revise-log-item'
+ import ReviseLogItemDetailDrawer from './modules/revise-log-item-detail-drawer.vue'
+ import { createReviseLogItemTableColumns } from './reviseLogItemTable.columns'
+ import {
+ buildReviseLogItemPageQueryParams,
+ buildReviseLogItemSearchParams,
+ createReviseLogItemSearchState,
+ getReviseLogItemPaginationKey,
+ normalizeReviseLogItemRow
+ } from './reviseLogItemPage.helpers'
+
+ defineOptions({ name: 'ReviseLogItem' })
+
+ const searchForm = ref(createReviseLogItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ簱浣�/鐗╂枡/鎵规' }
+ },
+ {
+ label: '鏃ュ織ID',
+ key: 'reviseLogId',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ棩蹇桰D' }
+ },
+ {
+ label: '搴撲綅缂栫爜',
+ key: 'locCode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�' }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�' }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�' }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ壒娆�' }
+ },
+ {
+ label: '鎵╁睍瀛楁',
+ key: 'fieldsIndex',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ墿灞曞瓧娈�' }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetailData(row.id)
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData } =
+ useTable({
+ core: {
+ apiFn: fetchReviseLogItemPage,
+ apiParams: buildReviseLogItemPageQueryParams(searchForm.value),
+ paginationKey: getReviseLogItemPaginationKey(),
+ columnsFactory: () => createReviseLogItemTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeReviseLogItemRow(item)) : [])
+ }
+ })
+
+ async function loadDetailData(id) {
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetReviseLogItemDetail(id), {}, {
+ timeoutMessage: '搴撲綅璋冩暣鏃ュ織鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeReviseLogItemRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撲綅璋冩暣鏃ュ織鏄庣粏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildReviseLogItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createReviseLogItemSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/manager/revise-log-item/modules/revise-log-item-detail-drawer.vue b/rsf-design/src/views/manager/revise-log-item/modules/revise-log-item-detail-drawer.vue
new file mode 100644
index 0000000..b428d47
--- /dev/null
+++ b/rsf-design/src/views/manager/revise-log-item/modules/revise-log-item-detail-drawer.vue
@@ -0,0 +1,48 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撲綅璋冩暣鏃ュ織鏄庣粏璇︽儏"
+ size="66%"
+ destroy-on-close
+ @update:model-value="emit('update:visible', $event)"
+ >
+ <ElSkeleton :loading="loading" animated :rows="8">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="鏃ュ織ID">{{ detail.reviseLogId ?? '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍘熷簱瀛�">{{ detail.anfme ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璋冩暣鏁伴噺">{{ detail.reviseQty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸紓鏁伴噺">{{ detail.diffQty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勬牸">{{ detail.spec || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍨嬪彿">{{ detail.model || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵╁睍瀛楁">{{ detail.fieldsIndex || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusTagType || 'info'" effect="light">
+ {{ detail.statusText || '-' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ detail.memo || '-' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ReviseLogItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+</script>
diff --git a/rsf-design/src/views/manager/revise-log-item/reviseLogItemPage.helpers.js b/rsf-design/src/views/manager/revise-log-item/reviseLogItemPage.helpers.js
new file mode 100644
index 0000000..e9a416f
--- /dev/null
+++ b/rsf-design/src/views/manager/revise-log-item/reviseLogItemPage.helpers.js
@@ -0,0 +1,128 @@
+const STATUS_OPTIONS = [
+ { label: '姝e父', value: 1, tagType: 'success' },
+ { label: '鍐荤粨', value: 0, tagType: 'danger' }
+]
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+function pickText(record, keys, fallback = '-') {
+ for (const key of keys) {
+ const value = record?.[key]
+ if (typeof value === 'string' && value.trim()) {
+ return value.trim()
+ }
+ }
+ return fallback
+}
+
+export const REVISE_LOG_ITEM_REPORT_TITLE = '搴撲綅璋冩暣鏃ュ織鏄庣粏'
+export const REVISE_LOG_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+export function getReviseLogItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function createReviseLogItemSearchState() {
+ return {
+ condition: '',
+ reviseLogId: '',
+ locCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ fieldsIndex: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function buildReviseLogItemSearchParams(params = {}) {
+ const result = {}
+ ;[
+ 'condition',
+ 'locCode',
+ 'matnrCode',
+ 'maktx',
+ 'batch',
+ 'fieldsIndex',
+ 'memo',
+ 'timeStart',
+ 'timeEnd'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['reviseLogId', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key], void 0)
+ if (value !== void 0) {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function buildReviseLogItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildReviseLogItemSearchParams(params)
+ }
+}
+
+export function getReviseLogItemStatusMeta(value) {
+ const target = STATUS_OPTIONS.find((item) => Number(item.value) === Number(value))
+ return target || { label: '-', value, tagType: 'info' }
+}
+
+export function normalizeReviseLogItemRow(record = {}) {
+ const statusMeta = getReviseLogItemStatusMeta(record.status)
+ const reviseQty = normalizeNumber(record.reviseQty, 0)
+ const anfme = normalizeNumber(record.anfme, 0)
+ return {
+ ...record,
+ reviseLogId: normalizeNumber(record.reviseLogId, record.reviseLogId),
+ locCode: pickText(record, ['locCode']),
+ matnrCode: pickText(record, ['matnrCode']),
+ maktx: pickText(record, ['maktx']),
+ unit: pickText(record, ['unit']),
+ anfme,
+ reviseQty,
+ diffQty: Math.round((reviseQty - anfme) * 10000) / 10000,
+ batch: pickText(record, ['batch']),
+ spec: pickText(record, ['spec']),
+ model: pickText(record, ['model']),
+ fieldsIndex: pickText(record, ['fieldsIndex']),
+ statusText: pickText(record, ['status$'], statusMeta.label),
+ statusTagType: statusMeta.tagType,
+ createByText: pickText(record, ['createBy$']),
+ createTimeText: pickText(record, ['createTime$'], record.createTime || '-'),
+ updateByText: pickText(record, ['updateBy$']),
+ updateTimeText: pickText(record, ['updateTime$'], record.updateTime || '-'),
+ memo: pickText(record, ['memo'], '-')
+ }
+}
diff --git a/rsf-design/src/views/manager/revise-log-item/reviseLogItemTable.columns.js b/rsf-design/src/views/manager/revise-log-item/reviseLogItemTable.columns.js
new file mode 100644
index 0000000..2c495b5
--- /dev/null
+++ b/rsf-design/src/views/manager/revise-log-item/reviseLogItemTable.columns.js
@@ -0,0 +1,260 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createReviseLogItemTableColumns({ handleView } = {}) {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'reviseLogId',
+ label: '鏃ュ織ID',
+ width: 92,
+ align: 'right'
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '鍘熷簱瀛�',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'reviseQty',
+ label: '璋冩暣鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'diffQty',
+ label: '宸紓鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: Number(row.diffQty) === 0 ? 'info' : Number(row.diffQty) > 0 ? 'success' : 'danger',
+ effect: 'light'
+ },
+ () => String(row.diffQty)
+ )
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'spec',
+ label: '瑙勬牸',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'model',
+ label: '鍨嬪彿',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '鎵╁睍瀛楁',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) =>
+ h(ElTag, { type: row.statusTagType || 'info', effect: 'light' }, () => row.statusText)
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 96,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
+
+export function createReviseLogItemDetailColumns() {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'reviseLogId',
+ label: '鏃ュ織ID',
+ width: 92,
+ align: 'right'
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '鍘熷簱瀛�',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'reviseQty',
+ label: '璋冩暣鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'diffQty',
+ label: '宸紓鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'spec',
+ label: '瑙勬牸',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'model',
+ label: '鍨嬪彿',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '鎵╁睍瀛楁',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/revise-log/index.vue b/rsf-design/src/views/manager/revise-log/index.vue
new file mode 100644
index 0000000..bb674ee
--- /dev/null
+++ b/rsf-design/src/views/manager/revise-log/index.vue
@@ -0,0 +1,266 @@
+<template>
+ <div class="revise-log-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <ReviseLogDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :summary="detailData"
+ :item-loading="itemLoading"
+ :item-data="itemTableData"
+ :item-columns="itemColumns"
+ :item-pagination="itemPagination"
+ @item-size-change="handleItemSizeChange"
+ @item-current-change="handleItemCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchGetReviseLogDetail, fetchReviseLogPage } from '@/api/revise-log'
+ import { fetchReviseLogItemPage } from '@/api/revise-log-item'
+ import ReviseLogDetailDrawer from './modules/revise-log-detail-drawer.vue'
+ import { createReviseLogTableColumns } from './reviseLogTable.columns'
+ import {
+ buildReviseLogPageQueryParams,
+ buildReviseLogSearchParams,
+ createReviseLogSearchState,
+ getReviseLogPaginationKey,
+ getReviseLogStatusOptions,
+ getReviseLogTypeOptions,
+ normalizeReviseLogRow
+ } from './reviseLogPage.helpers'
+ import { createReviseLogItemDetailColumns } from '../revise-log-item/reviseLogItemTable.columns'
+ import { normalizeReviseLogItemRow } from '../revise-log-item/reviseLogItemPage.helpers'
+
+ defineOptions({ name: 'ReviseLog' })
+
+ const searchForm = ref(createReviseLogSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const itemLoading = ref(false)
+ const itemTableData = ref([])
+ const activeDetail = ref({})
+ const itemPagination = reactive({ current: 1, size: 20, total: 0 })
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヨ皟鏁村崟鍙�/搴撲綅/瀹瑰櫒鐮�' }
+ },
+ {
+ label: '璋冩暣鍗曞彿',
+ key: 'reviseId',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヨ皟鏁村崟ID' }
+ },
+ {
+ label: '璋冩暣缂栫爜',
+ key: 'reviseCode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヨ皟鏁村崟缂栫爜' }
+ },
+ {
+ label: '浠撳簱ID',
+ key: 'warehouseId',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヤ粨搴揑D' }
+ },
+ {
+ label: '搴撳尯ID',
+ key: 'areaId',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ簱鍖篒D' }
+ },
+ {
+ label: '璋冩暣绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getReviseLogTypeOptions().map((item) => ({ label: item.label, value: item.value }))
+ }
+ },
+ {
+ label: '瀹瑰櫒鐮�',
+ key: 'barcode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ鍣ㄧ爜' }
+ },
+ {
+ label: '搴撲綅鐘舵��',
+ key: 'useStatus',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ簱浣嶇姸鎬�' }
+ },
+ {
+ label: '宸烽亾',
+ key: 'channel',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ贩閬�' }
+ },
+ {
+ label: '鎺�',
+ key: 'row',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ帓' }
+ },
+ {
+ label: '鍒�',
+ key: 'col',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ垪' }
+ },
+ {
+ label: '灞�',
+ key: 'lev',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ眰' }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getReviseLogStatusOptions().map((item) => ({ label: item.label, value: item.value }))
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
+ }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ activeDetail.value = row || {}
+ itemTableData.value = []
+ itemPagination.current = 1
+ try {
+ detailData.value = normalizeReviseLogRow(
+ await guardRequestWithMessage(fetchGetReviseLogDetail(row.id), {}, {
+ timeoutMessage: '搴撲綅璋冩暣鏃ュ織璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ )
+ await loadItemData(row.id)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撲綅璋冩暣鏃ュ織璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function loadItemData(reviseLogId = activeDetail.value?.id) {
+ if (!reviseLogId) {
+ itemTableData.value = []
+ return
+ }
+ itemLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchReviseLogItemPage({
+ reviseLogId,
+ current: itemPagination.current,
+ pageSize: itemPagination.size
+ }),
+ { records: [], total: 0, current: itemPagination.current, size: itemPagination.size },
+ { timeoutMessage: '搴撲綅璋冩暣鏃ュ織鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ const normalized = defaultResponseAdapter(response)
+ itemTableData.value = Array.isArray(normalized?.records)
+ ? normalized.records.map((record) => normalizeReviseLogItemRow(record))
+ : []
+ updatePaginationState(itemPagination, normalized, itemPagination.current, itemPagination.size)
+ } finally {
+ itemLoading.value = false
+ }
+ }
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData } =
+ useTable({
+ core: {
+ apiFn: fetchReviseLogPage,
+ apiParams: buildReviseLogPageQueryParams(searchForm.value),
+ paginationKey: getReviseLogPaginationKey(),
+ columnsFactory: () => createReviseLogTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeReviseLogRow(item)) : [])
+ }
+ })
+
+ const itemColumns = computed(() => createReviseLogItemDetailColumns())
+
+ function handleItemSizeChange(size) {
+ itemPagination.size = size
+ itemPagination.current = 1
+ loadItemData()
+ }
+
+ function handleItemCurrentChange(current) {
+ itemPagination.current = current
+ loadItemData()
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildReviseLogSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createReviseLogSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/manager/revise-log/modules/revise-log-detail-drawer.vue b/rsf-design/src/views/manager/revise-log/modules/revise-log-detail-drawer.vue
new file mode 100644
index 0000000..1cf41bd
--- /dev/null
+++ b/rsf-design/src/views/manager/revise-log/modules/revise-log-detail-drawer.vue
@@ -0,0 +1,74 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撲綅璋冩暣鏃ュ織璇︽儏"
+ size="82%"
+ destroy-on-close
+ @update:model-value="emit('update:visible', $event)"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElSkeleton :loading="loading" animated :rows="8">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="璋冩暣鍗旾D">{{ summary.reviseId ?? '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璋冩暣鍗曠紪鐮�">{{ summary.reviseCode || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱">{{ summary.warehouseLabel || summary.warehouseId || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯">{{ summary.areaLabel || summary.areaId || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ summary.locCode || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璋冩暣绫诲瀷">{{ summary.typeLabel || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹瑰櫒鐮�">{{ summary.barcode || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅鐘舵��">{{ summary.useStatusText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="summary.statusTagType || 'info'" effect="light">
+ {{ summary.statusText || '-' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="宸烽亾">{{ summary.channelText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎺�">{{ summary.rowText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒�">{{ summary.colText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="灞�">{{ summary.levText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ summary.createByText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ summary.createTimeText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ summary.updateByText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ summary.updateTimeText || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ summary.memo || '-' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+
+ <div class="flex min-h-0 flex-1 flex-col gap-3">
+ <div class="flex items-center justify-between">
+ <h3 class="text-base font-semibold text-g-900">璋冩暣鏃ュ織鏄庣粏</h3>
+ <span class="text-sm text-g-500">鍏� {{ itemPagination.total || 0 }} 鏉�</span>
+ </div>
+
+ <ArtTable
+ :loading="itemLoading"
+ :data="itemData"
+ :columns="itemColumns"
+ :pagination="itemPagination"
+ @pagination:size-change="emit('item-size-change', $event)"
+ @pagination:current-change="emit('item-current-change', $event)"
+ />
+ </div>
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ReviseLogDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ summary: { type: Object, default: () => ({}) },
+ itemLoading: { type: Boolean, default: false },
+ itemData: { type: Array, default: () => [] },
+ itemColumns: { type: Array, default: () => [] },
+ itemPagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits([
+ 'update:visible',
+ 'item-size-change',
+ 'item-current-change'
+ ])
+</script>
diff --git a/rsf-design/src/views/manager/revise-log/reviseLogPage.helpers.js b/rsf-design/src/views/manager/revise-log/reviseLogPage.helpers.js
new file mode 100644
index 0000000..6d57d46
--- /dev/null
+++ b/rsf-design/src/views/manager/revise-log/reviseLogPage.helpers.js
@@ -0,0 +1,151 @@
+const TYPE_OPTIONS = [
+ { label: '搴撳瓨璋冩暣', value: 0, tagType: 'primary' },
+ { label: '鐩樼偣璋冩暣', value: 1, tagType: 'warning' },
+ { label: '鍏跺畠璋冩暣', value: 2, tagType: 'info' }
+]
+
+const STATUS_OPTIONS = [
+ { label: '姝e父', value: 1, tagType: 'success' },
+ { label: '鍐荤粨', value: 0, tagType: 'danger' }
+]
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+function pickText(record, keys, fallback = '-') {
+ for (const key of keys) {
+ const value = record?.[key]
+ if (typeof value === 'string' && value.trim()) {
+ return value.trim()
+ }
+ }
+ return fallback
+}
+
+export const REVISE_LOG_REPORT_TITLE = '搴撲綅璋冩暣鏃ュ織'
+export const REVISE_LOG_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+export function getReviseLogTypeOptions() {
+ return TYPE_OPTIONS
+}
+
+export function getReviseLogStatusOptions() {
+ return STATUS_OPTIONS
+}
+
+export function getReviseLogPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function createReviseLogSearchState() {
+ return {
+ condition: '',
+ reviseId: '',
+ reviseCode: '',
+ warehouseId: '',
+ areaId: '',
+ type: '',
+ barcode: '',
+ useStatus: '',
+ channel: '',
+ row: '',
+ col: '',
+ lev: '',
+ memo: '',
+ status: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function buildReviseLogSearchParams(params = {}) {
+ const result = {}
+ ;[
+ 'condition',
+ 'reviseCode',
+ 'barcode',
+ 'useStatus',
+ 'memo',
+ 'timeStart',
+ 'timeEnd'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['reviseId', 'warehouseId', 'areaId', 'type', 'channel', 'row', 'col', 'lev', 'status'].forEach(
+ (key) => {
+ const value = normalizeNumber(params[key], void 0)
+ if (value !== void 0) {
+ result[key] = value
+ }
+ }
+ )
+
+ return result
+}
+
+export function buildReviseLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildReviseLogSearchParams(params)
+ }
+}
+
+export function getReviseLogTypeMeta(value) {
+ const target = TYPE_OPTIONS.find((item) => Number(item.value) === Number(value))
+ return target || { label: '-', value, tagType: 'info' }
+}
+
+export function getReviseLogStatusMeta(value) {
+ const target = STATUS_OPTIONS.find((item) => Number(item.value) === Number(value))
+ return target || { label: '-', value, tagType: 'info' }
+}
+
+export function normalizeReviseLogRow(record = {}) {
+ const typeMeta = getReviseLogTypeMeta(record.type)
+ const statusMeta = getReviseLogStatusMeta(record.status)
+ return {
+ ...record,
+ reviseId: normalizeNumber(record.reviseId, record.reviseId),
+ reviseCode: pickText(record, ['reviseCode', 'code']),
+ warehouseLabel: pickText(record, ['warehouseId$', 'warehouseName', 'warehouseLabel']),
+ areaLabel: pickText(record, ['areaId$', 'areaName', 'areaLabel']),
+ locCode: pickText(record, ['locCode']),
+ typeLabel: pickText(record, ['type$'], typeMeta.label),
+ barcode: pickText(record, ['barcode']),
+ useStatusText: pickText(record, ['useStatus$'], record.useStatus || '-'),
+ channelText: pickText(record, ['channel$'], record.channel ?? '-'),
+ rowText: pickText(record, ['row$'], record.row ?? '-'),
+ colText: pickText(record, ['col$'], record.col ?? '-'),
+ levText: pickText(record, ['lev$'], record.lev ?? '-'),
+ statusText: pickText(record, ['status$'], statusMeta.label),
+ statusTagType: statusMeta.tagType,
+ createByText: pickText(record, ['createBy$']),
+ createTimeText: pickText(record, ['createTime$'], record.createTime || '-'),
+ updateByText: pickText(record, ['updateBy$']),
+ updateTimeText: pickText(record, ['updateTime$'], record.updateTime || '-'),
+ memo: pickText(record, ['memo'], '-')
+ }
+}
diff --git a/rsf-design/src/views/manager/revise-log/reviseLogTable.columns.js b/rsf-design/src/views/manager/revise-log/reviseLogTable.columns.js
new file mode 100644
index 0000000..bc41d5f
--- /dev/null
+++ b/rsf-design/src/views/manager/revise-log/reviseLogTable.columns.js
@@ -0,0 +1,127 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createReviseLogTableColumns({ handleView } = {}) {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'reviseCode',
+ label: '璋冩暣鍗曞彿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'warehouseLabel',
+ label: '浠撳簱',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'areaLabel',
+ label: '搴撳尯',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'typeLabel',
+ label: '璋冩暣绫诲瀷',
+ minWidth: 110
+ },
+ {
+ prop: 'barcode',
+ label: '瀹瑰櫒鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'useStatusText',
+ label: '搴撲綅鐘舵��',
+ minWidth: 110
+ },
+ {
+ prop: 'channelText',
+ label: '宸烽亾',
+ width: 90,
+ align: 'right'
+ },
+ {
+ prop: 'rowText',
+ label: '鎺�',
+ width: 80,
+ align: 'right'
+ },
+ {
+ prop: 'colText',
+ label: '鍒�',
+ width: 80,
+ align: 'right'
+ },
+ {
+ prop: 'levText',
+ label: '灞�',
+ width: 80,
+ align: 'right'
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) =>
+ h(ElTag, { type: row.statusTagType || 'info', effect: 'light' }, () => row.statusText)
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 110,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 110,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 96,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/stock-item/index.vue b/rsf-design/src/views/manager/stock-item/index.vue
new file mode 100644
index 0000000..163acd7
--- /dev/null
+++ b/rsf-design/src/views/manager/stock-item/index.vue
@@ -0,0 +1,492 @@
+<template>
+ <div class="stock-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板搴撳瓨鏄庣粏</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <span v-auth="'list'" class="inline-flex">
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </span>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ <template #action="{ row }">
+ <ArtButtonTable icon="ri:eye-line" @click="openDetail(row)" />
+ <ArtButtonTable v-auth="'edit'" icon="ri:pencil-line" @click="openEditDialog(row)" />
+ <ArtButtonTable v-auth="'delete'" icon="ri:delete-bin-5-line" @click="handleDeleteAction(row)" />
+ </template>
+ </ArtTable>
+ </ElCard>
+
+ <StockItemDialog
+ v-model:visible="dialogVisible"
+ :stock-item-data="currentStockItemData"
+ :loading="dialogSubmitting"
+ @submit="handleDialogSubmit"
+ />
+
+ <StockItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+ import {
+ fetchDeleteStockItem,
+ fetchExportStockItemReport,
+ fetchStockItemDetail,
+ fetchStockItemMany,
+ fetchSaveStockItem,
+ fetchStockItemPage,
+ fetchUpdateStockItem
+ } from '@/api/stock-item'
+ import StockItemDetailDrawer from './modules/stock-item-detail-drawer.vue'
+ import StockItemDialog from './modules/stock-item-dialog.vue'
+ import { createStockItemTableColumns } from './stockItemTable.columns.js'
+ import {
+ buildStockItemDialogModel,
+ buildStockItemPageQueryParams,
+ buildStockItemPrintRows,
+ buildStockItemReportMeta,
+ buildStockItemSavePayload,
+ buildStockItemSearchParams,
+ createStockItemSearchState,
+ getStockItemPaginationKey,
+ normalizeStockItemRow,
+ STOCK_ITEM_REPORT_STYLE,
+ STOCK_ITEM_REPORT_TITLE
+ } from './stockItemPage.helpers.js'
+
+ defineOptions({ name: 'StockItem' })
+
+ const userStore = useUserStore()
+ const reportTitle = STOCK_ITEM_REPORT_TITLE
+ const searchForm = ref(createStockItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const dialogSubmitting = ref(false)
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ富鍗曠紪鍙�/鐗╂枡缂栫爜/鐗╂枡鍚嶇О'
+ }
+ },
+ {
+ label: '涓诲崟缂栧彿',
+ key: 'stockCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ富鍗曠紪鍙�'
+ }
+ },
+ {
+ label: '涓诲崟ID',
+ key: 'stockId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ富鍗旾D'
+ }
+ },
+ {
+ label: '鏄庣粏ID',
+ key: 'sourceItemId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ槑缁咺D'
+ }
+ },
+ {
+ label: '鐗╂枡ID',
+ key: 'matnrId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ墿鏂橧D'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '閫佽揣鏁伴噺',
+ key: 'anfme',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ラ�佽揣鏁伴噺'
+ }
+ },
+ {
+ label: '搴撳瓨鍗曚綅',
+ key: 'stockUnit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樺崟浣�'
+ }
+ },
+ {
+ label: '鎵ц涓暟閲�',
+ key: 'workQty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ墽琛屼腑鏁伴噺'
+ }
+ },
+ {
+ label: '閲囪喘鏁伴噺',
+ key: 'purQty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ラ噰璐暟閲�'
+ }
+ },
+ {
+ label: '閲囪喘鍗曚綅',
+ key: 'purUnit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ噰璐崟浣�'
+ }
+ },
+ {
+ label: '宸叉敹鏁伴噺',
+ key: 'qty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ凡鏀舵暟閲�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗙紪鐮�',
+ key: 'splrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢缂栫爜'
+ }
+ },
+ {
+ label: '搴撳瓨鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樻壒娆�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗗悕绉�',
+ key: 'splrName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鍚嶇О'
+ }
+ },
+ {
+ label: '璺熻釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ窡韪爜'
+ }
+ },
+ {
+ label: '鏉″舰鐮�',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯褰㈢爜'
+ }
+ },
+ {
+ label: '鐢熶骇鏃ユ湡',
+ key: 'prodTime',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ敓浜ф棩鏈�'
+ }
+ },
+ {
+ label: '鍖呰鍚嶇О',
+ key: 'packName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ寘瑁呭悕绉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ }
+ ])
+
+ const reportQueryParams = computed(() => buildStockItemSearchParams(searchForm.value))
+
+ let handleDeleteActionImpl = null
+ let openEditDialogActionImpl = null
+
+ const handleDeleteAction = (row) => handleDeleteActionImpl?.(row)
+ const openEditDialog = (row) => openEditDialogActionImpl?.(row)
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
+ useTable({
+ core: {
+ apiFn: fetchStockItemPage,
+ apiParams: buildStockItemPageQueryParams(searchForm.value),
+ paginationKey: getStockItemPaginationKey(),
+ columnsFactory: () =>
+ createStockItemTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: handleDeleteAction
+ })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeStockItemRow(item)) : [])
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentStockItemData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDelete,
+ handleBatchDelete,
+ handleDialogSubmit: crudHandleDialogSubmit
+ } = useCrudPage({
+ createEmptyModel: () => buildStockItemDialogModel(),
+ buildEditModel: (record) => buildStockItemDialogModel(record),
+ buildSavePayload: (formData) => buildStockItemSavePayload(formData),
+ saveRequest: fetchSaveStockItem,
+ updateRequest: fetchUpdateStockItem,
+ deleteRequest: fetchDeleteStockItem,
+ entityName: '搴撳瓨鏄庣粏',
+ resolveRecordLabel: (record) => record?.stockCode || record?.maktx || record?.matnrCode || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+
+ handleDeleteActionImpl = handleDelete
+ openEditDialogActionImpl = (row) => showDialog('edit', row)
+
+ async function handleDialogSubmit(formData) {
+ dialogSubmitting.value = true
+ try {
+ await crudHandleDialogSubmit(formData)
+ } finally {
+ dialogSubmitting.value = false
+ }
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeStockItemRow(
+ await guardRequestWithMessage(fetchStockItemDetail(row.id), {}, {
+ timeoutMessage: '搴撳瓨鏄庣粏璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ )
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撳瓨鏄庣粏璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildStockItemPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createStockItemSearchState())
+ resetSearchParams()
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'stock-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportStockItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords: async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchStockItemMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchStockItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ },
+ buildPreviewRows: (records) => buildStockItemPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...STOCK_ITEM_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildStockItemReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || STOCK_ITEM_REPORT_STYLE.orientation
+ })
+ )
+
+ onMounted(() => {
+ getData()
+ })
+</script>
diff --git a/rsf-design/src/views/manager/stock-item/modules/stock-item-detail-drawer.vue b/rsf-design/src/views/manager/stock-item/modules/stock-item-detail-drawer.vue
new file mode 100644
index 0000000..a597ddb
--- /dev/null
+++ b/rsf-design/src/views/manager/stock-item/modules/stock-item-detail-drawer.vue
@@ -0,0 +1,82 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳瓨鏄庣粏璇︽儏"
+ size="72%"
+ destroy-on-close
+ append-to-body
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="涓诲崟ID">{{ displayData.stockId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓诲崟缂栧彿">{{ displayData.stockCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏄庣粏ID">{{ displayData.sourceItemId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡ID">{{ displayData.matnrId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ displayData.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ displayData.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閫佽揣鏁伴噺">{{ displayData.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鍗曚綅">{{ displayData.stockUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц涓暟閲�">{{ displayData.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘鏁伴噺">{{ displayData.purQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘鍗曚綅">{{ displayData.purUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸叉敹鏁伴噺">{{ displayData.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗙紪鐮�">{{ displayData.splrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟咺D">{{ displayData.splrId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗗悕绉�">{{ displayData.splrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鎵规">{{ displayData.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ displayData.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璺熻釜鐮�">{{ displayData.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉″舰鐮�">{{ displayData.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐢熶骇鏃ユ湡">{{ displayData.prodTime || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍖呰鍚嶇О">{{ displayData.packName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁绱㈠紩">{{ displayData.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴琛屽彿">{{ displayData.platItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛璁㈠崟鍙�">{{ displayData.platOrderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ュ崟鍙�">{{ displayData.platWorkCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰鍙�">{{ displayData.projectCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍩烘湰鍗曚綅">{{ displayData.baseUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤缁勭粐ID">{{ displayData.useOrgId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤缁勭粐鍚嶇О">{{ displayData.useOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺灞炴��">{{ displayData.erpClsId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁′环鍗曚綅">{{ displayData.priceUnitId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍏ュ簱绫诲瀷">{{ displayData.inStockType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐т富绫诲瀷">{{ displayData.ownerType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusTagType || 'info'" effect="light">
+ {{ displayData.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇敼浜�">{{ displayData.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇敼鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElSkeleton>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import { normalizeStockItemRow } from '../stockItemPage.helpers.js'
+
+ defineOptions({ name: 'StockItemDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeStockItemRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/stock-item/modules/stock-item-dialog.vue b/rsf-design/src/views/manager/stock-item/modules/stock-item-dialog.vue
new file mode 100644
index 0000000..8630afd
--- /dev/null
+++ b/rsf-design/src/views/manager/stock-item/modules/stock-item-dialog.vue
@@ -0,0 +1,450 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="1040px"
+ align-center
+ destroy-on-close
+ append-to-body
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ElScrollbar class="max-h-[calc(100vh-220px)] pr-2">
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+ </ElScrollbar>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" :loading="loading" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildStockItemDialogModel,
+ createStockItemFormState,
+ getStockItemStatusOptions
+ } from '../stockItemPage.helpers.js'
+
+ defineOptions({ name: 'StockItemDialog' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ stockItemData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createStockItemFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫搴撳瓨鏄庣粏' : '鏂板搴撳瓨鏄庣粏'))
+
+ const rules = computed(() => ({
+ stockCode: [{ required: true, message: '璇疯緭鍏ヤ富鍗曠紪鍙�', trigger: 'blur' }],
+ matnrCode: [{ required: true, message: '璇疯緭鍏ョ墿鏂欑紪鐮�', trigger: 'blur' }],
+ maktx: [{ required: true, message: '璇疯緭鍏ョ墿鏂欏悕绉�', trigger: 'blur' }],
+ status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: 'ID',
+ key: 'id',
+ type: 'input',
+ props: {
+ disabled: true,
+ placeholder: '鏂板鍚庤嚜鍔ㄧ敓鎴�'
+ }
+ },
+ {
+ label: '涓诲崟ID',
+ key: 'stockId',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ富鍗旾D'
+ }
+ },
+ {
+ label: '涓诲崟缂栧彿',
+ key: 'stockCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ富鍗曠紪鍙�'
+ }
+ },
+ {
+ label: '鏄庣粏ID',
+ key: 'sourceItemId',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ槑缁咺D'
+ }
+ },
+ {
+ label: '鐗╂枡ID',
+ key: 'matnrId',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ墿鏂橧D'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '閫佽揣鏁伴噺',
+ key: 'anfme',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ラ�佽揣鏁伴噺'
+ }
+ },
+ {
+ label: '搴撳瓨鍗曚綅',
+ key: 'stockUnit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樺崟浣�'
+ }
+ },
+ {
+ label: '鎵ц涓暟閲�',
+ key: 'workQty',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ墽琛屼腑鏁伴噺'
+ }
+ },
+ {
+ label: '閲囪喘鏁伴噺',
+ key: 'purQty',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ラ噰璐暟閲�'
+ }
+ },
+ {
+ label: '閲囪喘鍗曚綅',
+ key: 'purUnit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ噰璐崟浣�'
+ }
+ },
+ {
+ label: '宸叉敹鏁伴噺',
+ key: 'qty',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ凡鏀舵暟閲�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗙紪鐮�',
+ key: 'splrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢缂栫爜'
+ }
+ },
+ {
+ label: '渚涘簲鍟咺D',
+ key: 'splrId',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢ID'
+ }
+ },
+ {
+ label: '骞冲彴琛屽彿',
+ key: 'platItemId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ钩鍙拌鍙�'
+ }
+ },
+ {
+ label: '瀹㈡埛璁㈠崟鍙�',
+ key: 'platOrderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鎴疯鍗曞彿'
+ }
+ },
+ {
+ label: '宸ュ崟鍙�',
+ key: 'platWorkCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ伐鍗曞彿'
+ }
+ },
+ {
+ label: '椤圭洰鍙�',
+ key: 'projectCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ」鐩彿'
+ }
+ },
+ {
+ label: '搴撳瓨鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樻壒娆�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗗悕绉�',
+ key: 'splrName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鍚嶇О'
+ }
+ },
+ {
+ label: '璺熻釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ窡韪爜'
+ }
+ },
+ {
+ label: '瀛楁绱㈠紩',
+ key: 'fieldsIndex',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈电储寮�'
+ }
+ },
+ {
+ label: '鏉″舰鐮�',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯褰㈢爜'
+ }
+ },
+ {
+ label: '鐢熶骇鏃ユ湡',
+ key: 'prodTime',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ敓浜ф棩鏈�'
+ }
+ },
+ {
+ label: '鍖呰鍚嶇О',
+ key: 'packName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ寘瑁呭悕绉�'
+ }
+ },
+ {
+ label: '鍩烘湰鍗曚綅',
+ key: 'baseUnit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ熀鏈崟浣�'
+ }
+ },
+ {
+ label: '浣跨敤缁勭粐ID',
+ key: 'useOrgId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ娇鐢ㄧ粍缁嘔D'
+ }
+ },
+ {
+ label: '浣跨敤缁勭粐鍚嶇О',
+ key: 'useOrgName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ娇鐢ㄧ粍缁囧悕绉�'
+ }
+ },
+ {
+ label: '鏁伴噺灞炴��',
+ key: 'erpClsId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ暟閲忓睘鎬�'
+ }
+ },
+ {
+ label: '璁′环鍗曚綅',
+ key: 'priceUnitId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ浠峰崟浣�'
+ }
+ },
+ {
+ label: '鍏ュ簱绫诲瀷',
+ key: 'inStockType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ叆搴撶被鍨�'
+ }
+ },
+ {
+ label: '璐т富绫诲瀷',
+ key: 'ownerType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ揣涓荤被鍨�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: getStockItemStatusOptions(),
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createStockItemFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildStockItemDialogModel(props.stockItemData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.stockItemData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/manager/stock-item/stockItemPage.helpers.js b/rsf-design/src/views/manager/stock-item/stockItemPage.helpers.js
new file mode 100644
index 0000000..6060078
--- /dev/null
+++ b/rsf-design/src/views/manager/stock-item/stockItemPage.helpers.js
@@ -0,0 +1,342 @@
+export const STOCK_ITEM_REPORT_TITLE = '搴撳瓨鏄庣粏鎶ヨ〃'
+export const STOCK_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const STOCK_ITEM_STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'danger' }
+}
+
+const TEXT_FIELDS = [
+ 'condition',
+ 'stockCode',
+ 'matnrCode',
+ 'maktx',
+ 'stockUnit',
+ 'purUnit',
+ 'splrCode',
+ 'batch',
+ 'splrBatch',
+ 'splrName',
+ 'trackCode',
+ 'barcode',
+ 'prodTime',
+ 'packName',
+ 'memo'
+]
+
+const NUMBER_FIELDS = ['stockId', 'sourceItemId', 'matnrId', 'anfme', 'workQty', 'purQty', 'qty', 'status']
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function pushText(target, key, value) {
+ const normalizedValue = normalizeText(value)
+ if (normalizedValue) {
+ target[key] = normalizedValue
+ }
+}
+
+function pushNumber(target, key, value) {
+ const normalizedValue = normalizeNumber(value)
+ if (normalizedValue !== null) {
+ target[key] = normalizedValue
+ }
+}
+
+function getStatusMeta(status, statusText) {
+ const numericStatus = Number(status)
+ return STOCK_ITEM_STATUS_META[numericStatus] || {
+ text: normalizeText(statusText || status || '--') || '--',
+ type: 'info'
+ }
+}
+
+export function createStockItemSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ stockId: '',
+ stockCode: '',
+ sourceItemId: '',
+ matnrId: '',
+ matnrCode: '',
+ maktx: '',
+ anfme: '',
+ stockUnit: '',
+ workQty: '',
+ purQty: '',
+ purUnit: '',
+ qty: '',
+ splrCode: '',
+ batch: '',
+ splrBatch: '',
+ splrName: '',
+ trackCode: '',
+ barcode: '',
+ prodTime: '',
+ packName: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function createStockItemFormState() {
+ return {
+ id: null,
+ stockId: '',
+ stockCode: '',
+ sourceItemId: '',
+ matnrId: '',
+ matnrCode: '',
+ maktx: '',
+ anfme: '',
+ stockUnit: '',
+ workQty: '',
+ purQty: '',
+ purUnit: '',
+ qty: '',
+ splrCode: '',
+ splrId: '',
+ platItemId: '',
+ platOrderCode: '',
+ platWorkCode: '',
+ projectCode: '',
+ batch: '',
+ splrBatch: '',
+ splrName: '',
+ trackCode: '',
+ fieldsIndex: '',
+ barcode: '',
+ prodTime: '',
+ packName: '',
+ baseUnit: '',
+ useOrgId: '',
+ useOrgName: '',
+ erpClsId: '',
+ priceUnitId: '',
+ inStockType: '',
+ ownerType: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getStockItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getStockItemStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function buildStockItemSearchParams(params = {}) {
+ const result = {}
+
+ TEXT_FIELDS.forEach((key) => pushText(result, key, params[key]))
+ NUMBER_FIELDS.forEach((key) => pushNumber(result, key, params[key]))
+
+ if (params.timeStart) {
+ result.timeStart = normalizeText(params.timeStart)
+ }
+ if (params.timeEnd) {
+ result.timeEnd = normalizeText(params.timeEnd)
+ }
+
+ return result
+}
+
+export function buildStockItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildStockItemSearchParams(params)
+ }
+}
+
+export function buildStockItemSavePayload(formData = {}) {
+ const payload = {}
+
+ pushNumber(payload, 'id', formData.id)
+ pushNumber(payload, 'stockId', formData.stockId)
+ pushNumber(payload, 'sourceItemId', formData.sourceItemId)
+ pushNumber(payload, 'matnrId', formData.matnrId)
+ pushNumber(payload, 'anfme', formData.anfme)
+ pushNumber(payload, 'workQty', formData.workQty)
+ pushNumber(payload, 'purQty', formData.purQty)
+ pushNumber(payload, 'qty', formData.qty)
+ pushNumber(payload, 'splrId', formData.splrId)
+ pushNumber(payload, 'status', formData.status ?? 1)
+
+ ;[
+ 'stockCode',
+ 'matnrCode',
+ 'maktx',
+ 'stockUnit',
+ 'purUnit',
+ 'splrCode',
+ 'platItemId',
+ 'platOrderCode',
+ 'platWorkCode',
+ 'projectCode',
+ 'batch',
+ 'splrBatch',
+ 'splrName',
+ 'trackCode',
+ 'fieldsIndex',
+ 'barcode',
+ 'prodTime',
+ 'packName',
+ 'baseUnit',
+ 'useOrgId',
+ 'useOrgName',
+ 'erpClsId',
+ 'priceUnitId',
+ 'inStockType',
+ 'ownerType',
+ 'memo'
+ ].forEach((key) => pushText(payload, key, formData[key]))
+
+ return payload
+}
+
+export function buildStockItemDialogModel(record = {}) {
+ return {
+ ...createStockItemFormState(),
+ id: record.id ?? null,
+ stockId: record.stockId ?? '',
+ stockCode: normalizeText(record.stockCode),
+ sourceItemId: record.sourceItemId ?? '',
+ matnrId: record.matnrId ?? '',
+ matnrCode: normalizeText(record.matnrCode),
+ maktx: normalizeText(record.maktx),
+ anfme: record.anfme ?? '',
+ stockUnit: normalizeText(record.stockUnit),
+ workQty: record.workQty ?? '',
+ purQty: record.purQty ?? '',
+ purUnit: normalizeText(record.purUnit),
+ qty: record.qty ?? '',
+ splrCode: normalizeText(record.splrCode),
+ splrId: record.splrId ?? '',
+ platItemId: normalizeText(record.platItemId),
+ platOrderCode: normalizeText(record.platOrderCode),
+ platWorkCode: normalizeText(record.platWorkCode),
+ projectCode: normalizeText(record.projectCode),
+ batch: normalizeText(record.batch),
+ splrBatch: normalizeText(record.splrBatch),
+ splrName: normalizeText(record.splrName),
+ trackCode: normalizeText(record.trackCode),
+ fieldsIndex: normalizeText(record.fieldsIndex),
+ barcode: normalizeText(record.barcode),
+ prodTime: normalizeText(record.prodTime),
+ packName: normalizeText(record.packName),
+ baseUnit: normalizeText(record.baseUnit),
+ useOrgId: normalizeText(record.useOrgId),
+ useOrgName: normalizeText(record.useOrgName),
+ erpClsId: normalizeText(record.erpClsId),
+ priceUnitId: normalizeText(record.priceUnitId),
+ inStockType: normalizeText(record.inStockType),
+ ownerType: normalizeText(record.ownerType),
+ status: record.status ?? 1,
+ memo: normalizeText(record.memo)
+ }
+}
+
+export function getStockItemStatusMeta(status, statusText) {
+ return getStatusMeta(status, statusText)
+}
+
+export function normalizeStockItemRow(record = {}) {
+ const statusMeta = getStatusMeta(record.status, record['status$'] || record.statusText)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ stockId: record.stockId ?? '--',
+ stockCode: normalizeText(record.stockCode) || '--',
+ sourceItemId: record.sourceItemId ?? '--',
+ matnrId: record.matnrId ?? '--',
+ matnrCode: normalizeText(record.matnrCode) || '--',
+ maktx: normalizeText(record.maktx) || '--',
+ anfme: Number.isFinite(Number(record.anfme)) ? Number(record.anfme) : 0,
+ stockUnit: normalizeText(record.stockUnit) || '--',
+ workQty: Number.isFinite(Number(record.workQty)) ? Number(record.workQty) : 0,
+ purQty: Number.isFinite(Number(record.purQty)) ? Number(record.purQty) : 0,
+ purUnit: normalizeText(record.purUnit) || '--',
+ qty: Number.isFinite(Number(record.qty)) ? Number(record.qty) : 0,
+ splrCode: normalizeText(record.splrCode) || '--',
+ splrId: record.splrId ?? '--',
+ platItemId: normalizeText(record.platItemId) || '--',
+ platOrderCode: normalizeText(record.platOrderCode) || '--',
+ platWorkCode: normalizeText(record.platWorkCode) || '--',
+ projectCode: normalizeText(record.projectCode) || '--',
+ batch: normalizeText(record.batch) || '--',
+ splrBatch: normalizeText(record.splrBatch) || '--',
+ splrName: normalizeText(record.splrName) || '--',
+ trackCode: normalizeText(record.trackCode) || '--',
+ fieldsIndex: normalizeText(record.fieldsIndex) || '--',
+ barcode: normalizeText(record.barcode) || '--',
+ prodTime: normalizeText(record.prodTime) || '--',
+ packName: normalizeText(record.packName) || '--',
+ baseUnit: normalizeText(record.baseUnit) || '--',
+ useOrgId: normalizeText(record.useOrgId) || '--',
+ useOrgName: normalizeText(record.useOrgName) || '--',
+ erpClsId: normalizeText(record.erpClsId) || '--',
+ priceUnitId: normalizeText(record.priceUnitId) || '--',
+ inStockType: normalizeText(record.inStockType) || '--',
+ ownerType: normalizeText(record.ownerType) || '--',
+ statusText: statusMeta.text,
+ statusTagType: statusMeta.type,
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function buildStockItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeStockItemRow(record))
+}
+
+export function buildStockItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = STOCK_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: STOCK_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...STOCK_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/manager/stock-item/stockItemTable.columns.js b/rsf-design/src/views/manager/stock-item/stockItemTable.columns.js
new file mode 100644
index 0000000..4f5dca9
--- /dev/null
+++ b/rsf-design/src/views/manager/stock-item/stockItemTable.columns.js
@@ -0,0 +1,177 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+function renderStatus(row) {
+ return h(
+ ElTag,
+ {
+ type: row.statusTagType || 'info',
+ effect: 'light'
+ },
+ () => row.statusText || '--'
+ )
+}
+
+export function createStockItemTableColumns({ handleView, handleEdit, handleDelete } = {}) {
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'stockCode',
+ label: '涓诲崟缂栧彿',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'stockId',
+ label: '涓诲崟ID',
+ width: 100
+ },
+ {
+ prop: 'sourceItemId',
+ label: '鏄庣粏ID',
+ width: 100
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '閫佽揣鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'stockUnit',
+ label: '搴撳瓨鍗曚綅',
+ width: 100,
+ align: 'center'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120,
+ align: 'right'
+ },
+ {
+ prop: 'purQty',
+ label: '閲囪喘鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'purUnit',
+ label: '閲囪喘鍗曚綅',
+ width: 100,
+ align: 'center'
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'splrCode',
+ label: '渚涘簲鍟嗙紪鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟嗗悕绉�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '搴撳瓨鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'trackCode',
+ label: '璺熻釜鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'barcode',
+ label: '鏉″舰鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'prodTime',
+ label: '鐢熶骇鏃ユ湡',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'packName',
+ label: '鍖呰鍚嶇О',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 92,
+ align: 'center',
+ formatter: renderStatus
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'action',
+ label: '鎿嶄綔',
+ width: 132,
+ fixed: 'right',
+ align: 'center',
+ useSlot: true
+ }
+ ]
+}
+
+export { ArtButtonTable }
diff --git a/rsf-design/src/views/manager/stock/index.vue b/rsf-design/src/views/manager/stock/index.vue
new file mode 100644
index 0000000..8639bed
--- /dev/null
+++ b/rsf-design/src/views/manager/stock/index.vue
@@ -0,0 +1,278 @@
+<template>
+ <div class="stock-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData">
+ <template #left>
+ <ElSpace wrap>
+ <span v-auth="'list'" class="inline-flex">
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </span>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <StockDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref, reactive } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { fetchExportStockReport, fetchGetStockDetail, fetchGetStockMany, fetchStockPage } from '@/api/stock'
+ import StockDetailDrawer from './modules/stock-detail-drawer.vue'
+ import { createStockTableColumns } from './stockTable.columns'
+ import {
+ buildStockPageQueryParams,
+ buildStockPrintRows,
+ buildStockReportMeta,
+ buildStockSearchParams,
+ createStockSearchState,
+ normalizeStockRow,
+ STOCK_REPORT_STYLE,
+ STOCK_REPORT_TITLE
+ } from './stockPage.helpers'
+
+ defineOptions({ name: 'Stock' })
+
+ const userStore = useUserStore()
+ const reportTitle = STOCK_REPORT_TITLE
+ const loading = ref(false)
+ const tableData = ref([])
+ const selectedRows = ref([])
+ const searchForm = ref(createStockSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildStockSearchParams(searchForm.value))
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ崟鎹紪鍙�/鏉ユ簮鍗曞彿' }
+ },
+ {
+ label: '鍗曟嵁缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ崟鎹紪鍙�' }
+ },
+ {
+ label: '鏉ユ簮鍗曞彿',
+ key: 'sourceCode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ潵婧愬崟鍙�' }
+ },
+ {
+ label: '鏉ユ簮鍗旾D',
+ key: 'sourceId',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ潵婧愬崟ID' }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'type',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�' }
+ },
+ {
+ label: '鍗曟嵁绫诲瀷',
+ key: 'wkType',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ崟鎹被鍨�' }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: { type: 'date', valueFormat: 'YYYY-MM-DD', placeholder: '璇烽�夋嫨寮�濮嬫椂闂�' }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: { type: 'date', valueFormat: 'YYYY-MM-DD', placeholder: '璇烽�夋嫨缁撴潫鏃堕棿' }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeStockRow(await fetchGetStockDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鍏ュ嚭搴撳巻鍙茶鎯呭け璐�')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ const { columns, columnChecks } = useTableColumns(() =>
+ createStockTableColumns({
+ handleView: openDetail
+ })
+ )
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchStockPage(
+ buildStockPageQueryParams({
+ ...searchForm.value,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ { records: [], total: 0, current: pagination.current, size: pagination.size },
+ { timeoutMessage: '鍏ュ嚭搴撳巻鍙插姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeStockRow(record))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ searchForm.value = createStockSearchState()
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'stock-history.xlsx',
+ requestExport: (payload) =>
+ fetchExportStockReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords: async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetStockMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchStockPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ },
+ buildPreviewRows: (records) => buildStockPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...STOCK_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildStockReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || STOCK_REPORT_STYLE.orientation
+ })
+ )
+
+ onMounted(() => {
+ loadPageData()
+ })
+</script>
diff --git a/rsf-design/src/views/manager/stock/modules/stock-detail-drawer.vue b/rsf-design/src/views/manager/stock/modules/stock-detail-drawer.vue
new file mode 100644
index 0000000..1fda184
--- /dev/null
+++ b/rsf-design/src/views/manager/stock/modules/stock-detail-drawer.vue
@@ -0,0 +1,36 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鍏ュ嚭搴撳巻鍙茶鎯�"
+ size="560px"
+ @update:model-value="emit('update:visible', $event)"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="鍗曟嵁缂栧彿">{{ detailData.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉ユ簮鍗曞彿">{{ detailData.sourceCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉ユ簮鍗旾D">{{ detailData.sourceId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detailData.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉$爜">{{ detailData.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detailData.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detailData.wkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detailData.anfme ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detailData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detailData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ detailData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'StockDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+</script>
diff --git a/rsf-design/src/views/manager/stock/stockPage.helpers.js b/rsf-design/src/views/manager/stock/stockPage.helpers.js
new file mode 100644
index 0000000..e3ab87e
--- /dev/null
+++ b/rsf-design/src/views/manager/stock/stockPage.helpers.js
@@ -0,0 +1,103 @@
+export const STOCK_REPORT_TITLE = '鍏ュ嚭搴撳巻鍙叉姤琛�'
+export const STOCK_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+export function createStockSearchState() {
+ return {
+ condition: '',
+ code: '',
+ sourceCode: '',
+ sourceId: '',
+ type: '',
+ wkType: '',
+ locCode: '',
+ barcode: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function buildStockSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'sourceCode', 'type', 'wkType', 'locCode', 'barcode', 'timeStart', 'timeEnd'].forEach(
+ (key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ }
+ )
+ if (params.sourceId !== '' && params.sourceId !== null && params.sourceId !== undefined) {
+ result.sourceId = normalizeNumber(params.sourceId)
+ }
+ return result
+}
+
+export function buildStockPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildStockSearchParams(params)
+ }
+}
+
+export function normalizeStockRow(record = {}) {
+ return {
+ ...record,
+ code: record.code || '-',
+ sourceCode: record.sourceCode || '-',
+ sourceId: record.sourceId ?? '-',
+ locCode: record.locCode || '-',
+ barcode: record.barcode || '-',
+ typeLabel: record['type$'] || record.type || '-',
+ wkTypeLabel: record['wkType$'] || record.wkType || '-',
+ anfme: normalizeNumber(record.anfme),
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createByText: record['createBy$'] || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ memo: record.memo || '-'
+ }
+}
+
+export function buildStockPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeStockRow(record))
+}
+
+export function buildStockReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = STOCK_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: STOCK_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...STOCK_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/manager/stock/stockTable.columns.js b/rsf-design/src/views/manager/stock/stockTable.columns.js
new file mode 100644
index 0000000..49a0f70
--- /dev/null
+++ b/rsf-design/src/views/manager/stock/stockTable.columns.js
@@ -0,0 +1,86 @@
+import { h } from 'vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createStockTableColumns({ handleView }) {
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'code',
+ label: '鍗曟嵁缂栧彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'sourceCode',
+ label: '鏉ユ簮鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'sourceId',
+ label: '鏉ユ簮鍗旾D',
+ width: 100
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'barcode',
+ label: '鏉$爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'typeLabel',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 120
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 110
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ fixed: 'right',
+ align: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/task-item-log/index.vue b/rsf-design/src/views/manager/task-item-log/index.vue
new file mode 100644
index 0000000..9ed97b5
--- /dev/null
+++ b/rsf-design/src/views/manager/task-item-log/index.vue
@@ -0,0 +1,389 @@
+<template>
+ <div class="task-item-log-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <TaskItemLogDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ fetchExportTaskItemLogReport,
+ fetchGetTaskItemLogDetail,
+ fetchGetTaskItemLogMany,
+ fetchTaskItemLogPage
+ } from '@/api/task-item-log'
+ import {
+ buildTaskItemLogPageQueryParams,
+ buildTaskItemLogPrintRows,
+ buildTaskItemLogSearchParams,
+ createTaskItemLogSearchState,
+ getTaskItemLogPaginationKey,
+ getTaskItemLogReportColumns,
+ normalizeTaskItemLogRow,
+ TASK_ITEM_LOG_REPORT_TITLE
+ } from './taskItemLogPage.helpers'
+ import { createTaskItemLogTableColumns } from './taskItemLogTable.columns'
+ import TaskItemLogDetailDrawer from './modules/task-item-log-detail-drawer.vue'
+
+ defineOptions({ name: 'TaskItemLog' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createTaskItemLogSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = TASK_ITEM_LOG_REPORT_TITLE
+ const reportQueryParams = computed(() => buildTaskItemLogSearchParams(searchForm.value))
+ const reportColumns = getTaskItemLogReportColumns()
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂�/浠诲姟/鍗曞彿鍏抽敭璇�'
+ }
+ },
+ {
+ label: '涓诲崟ID',
+ key: 'logId',
+ type: 'number',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ富鍗旾D'
+ }
+ },
+ {
+ label: '浠诲姟ID',
+ key: 'taskId',
+ type: 'number',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ换鍔D'
+ }
+ },
+ {
+ label: '浠诲姟鏄庣粏ID',
+ key: 'taskItemId',
+ type: 'number',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ换鍔℃槑缁咺D'
+ }
+ },
+ {
+ label: '鐗╂枡ID',
+ key: 'matnrId',
+ type: 'number',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂橧D'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '宸ュ崟鍙�',
+ key: 'platWorkCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ伐鍗曞彿'
+ }
+ },
+ {
+ label: '瀹㈡埛璁㈠崟鍙�',
+ key: 'platOrderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鎴疯鍗曞彿'
+ }
+ },
+ {
+ label: '椤圭洰鍙�',
+ key: 'projectCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ」鐩彿'
+ }
+ },
+ {
+ label: '婧愮紪鐮�',
+ key: 'source',
+ type: 'number',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ簮缂栫爜'
+ }
+ },
+ {
+ label: '婧愬崟鍙�',
+ key: 'sourceCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ簮鍗曞彿'
+ }
+ },
+ {
+ label: '婧愪富鍗旾D',
+ key: 'sourceId',
+ type: 'number',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ簮涓诲崟ID'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '搴撳瓨鍗曚綅',
+ key: 'unit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樺崟浣�'
+ }
+ },
+ {
+ label: '鏁伴噺',
+ key: 'anfme',
+ type: 'number',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ暟閲�'
+ }
+ },
+ {
+ label: '搴撳瓨鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樻壒娆�'
+ }
+ },
+ {
+ label: '瑙勬牸',
+ key: 'spec',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鏍�'
+ }
+ },
+ {
+ label: '鍨嬪彿',
+ key: 'model',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瀷鍙�'
+ }
+ },
+ {
+ label: '瀛楁绱㈠紩',
+ key: 'fieldsIndex',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈电储寮�'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchTaskItemLogPage,
+ apiParams: buildTaskItemLogPageQueryParams(searchForm.value),
+ paginationKey: getTaskItemLogPaginationKey(),
+ columnsFactory: () => createTaskItemLogTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeTaskItemLogRow(item)) : [])
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetTaskItemLogMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchTaskItemLogPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'task-item-log.xlsx',
+ requestExport: (payload) =>
+ fetchExportTaskItemLogReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTaskItemLogPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetTaskItemLogDetail(id)
+ detailData.value = normalizeTaskItemLogRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTaskItemLogSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTaskItemLogSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/manager/task-item-log/modules/task-item-log-detail-drawer.vue b/rsf-design/src/views/manager/task-item-log/modules/task-item-log-detail-drawer.vue
new file mode 100644
index 0000000..45a7dfa
--- /dev/null
+++ b/rsf-design/src/views/manager/task-item-log/modules/task-item-log-detail-drawer.vue
@@ -0,0 +1,72 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟宸ヤ綔鍘嗗彶妗h鎯�"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="涓诲崟ID">{{ detail.logId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟ID">{{ detail.taskId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鏄庣粏ID">{{ detail.taskItemId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡ID">{{ detail.matnrId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ュ崟鍙�">{{ detail.platWorkCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛璁㈠崟鍙�">{{ detail.platOrderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰鍙�">{{ detail.projectCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愮紪鐮�">{{ detail.source ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愬崟鍙�">{{ detail.sourceCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愪富鍗旾D">{{ detail.sourceId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勬牸">{{ detail.spec || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍨嬪彿">{{ detail.model || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍩烘湰鍗曚綅">{{ detail.baseUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤缁勭粐ID">{{ detail.useOrgId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浣跨敤缁勭粐">{{ detail.useOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺灞炴��">{{ detail.erpClsId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁′环鍗曚綅">{{ detail.priceUnitId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍏ュ簱绫诲瀷">{{ detail.inStockType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐т富绫诲瀷">{{ detail.ownerTypeId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐т富ID">{{ detail.ownerId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐т富鍚嶇О">{{ detail.ownerName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇濈鑰呯被鍨�">{{ detail.keeperTypeId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇濈鑰匢D">{{ detail.keeperId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇濈鑰呭悕绉�">{{ detail.keeperName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寤鸿鐩爣浠�">{{ detail.targetWarehouseId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寤鸿璋冨嚭浠�">{{ detail.sourceWarehouseId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'TaskItemLogDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/task-item-log/taskItemLogPage.helpers.js b/rsf-design/src/views/manager/task-item-log/taskItemLogPage.helpers.js
new file mode 100644
index 0000000..6438952
--- /dev/null
+++ b/rsf-design/src/views/manager/task-item-log/taskItemLogPage.helpers.js
@@ -0,0 +1,249 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const TASK_ITEM_LOG_REPORT_TITLE = '浠诲姟宸ヤ綔鍘嗗彶妗f姤琛�'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function normalizeReportText(value) {
+ return value === null || value === undefined || value === '' ? '--' : value
+}
+
+export function createTaskItemLogSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ logId: '',
+ taskId: '',
+ taskItemId: '',
+ matnrId: '',
+ maktx: '',
+ platItemId: '',
+ platOrderCode: '',
+ platWorkCode: '',
+ projectCode: '',
+ source: '',
+ sourceCode: '',
+ sourceId: '',
+ matnrCode: '',
+ unit: '',
+ anfme: '',
+ batch: '',
+ spec: '',
+ model: '',
+ fieldsIndex: '',
+ memo: '',
+ status: '',
+ baseUnit: '',
+ useOrgId: '',
+ useOrgName: '',
+ erpClsId: '',
+ priceUnitId: '',
+ inStockType: '',
+ ownerTypeId: '',
+ ownerId: '',
+ ownerName: '',
+ keeperTypeId: '',
+ keeperId: '',
+ keeperName: '',
+ targetWarehouseId: '',
+ sourceWarehouseId: ''
+ }
+}
+
+export function getTaskItemLogPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildTaskItemLogSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'maktx',
+ 'platItemId',
+ 'platOrderCode',
+ 'platWorkCode',
+ 'projectCode',
+ 'sourceCode',
+ 'matnrCode',
+ 'unit',
+ 'batch',
+ 'spec',
+ 'model',
+ 'fieldsIndex',
+ 'memo',
+ 'baseUnit',
+ 'useOrgId',
+ 'useOrgName',
+ 'erpClsId',
+ 'priceUnitId',
+ 'inStockType',
+ 'ownerTypeId',
+ 'ownerId',
+ 'ownerName',
+ 'keeperTypeId',
+ 'keeperId',
+ 'keeperName',
+ 'targetWarehouseId',
+ 'sourceWarehouseId'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;['logId', 'taskId', 'taskItemId', 'matnrId', 'source', 'sourceId', 'anfme', 'status'].forEach(
+ (key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ }
+ )
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildTaskItemLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTaskItemLogSearchParams(params)
+ }
+}
+
+export function getTaskItemLogStatusMeta(value) {
+ return STATUS_META[Number(value)] || { text: '-', type: 'info' }
+}
+
+export function normalizeTaskItemLogRow(record = {}) {
+ const statusMeta = getTaskItemLogStatusMeta(record.status)
+ return {
+ ...record,
+ logId: record.logId ?? '--',
+ taskId: record.taskId ?? '--',
+ taskItemId: record.taskItemId ?? '--',
+ matnrId: record.matnrId ?? '--',
+ maktx: normalizeReportText(record.maktx),
+ platItemId: normalizeReportText(record.platItemId),
+ platOrderCode: normalizeReportText(record.platOrderCode),
+ platWorkCode: normalizeReportText(record.platWorkCode),
+ projectCode: normalizeReportText(record.projectCode),
+ source: record.source ?? '--',
+ sourceCode: normalizeReportText(record.sourceCode),
+ sourceId: record.sourceId ?? '--',
+ matnrCode: normalizeReportText(record.matnrCode),
+ unit: normalizeReportText(record.unit),
+ anfme: record.anfme ?? '--',
+ batch: normalizeReportText(record.batch),
+ spec: normalizeReportText(record.spec),
+ model: normalizeReportText(record.model),
+ fieldsIndex: normalizeReportText(record.fieldsIndex),
+ baseUnit: normalizeReportText(record.baseUnit),
+ useOrgId: normalizeReportText(record.useOrgId),
+ useOrgName: normalizeReportText(record.useOrgName),
+ erpClsId: normalizeReportText(record.erpClsId),
+ priceUnitId: normalizeReportText(record.priceUnitId),
+ inStockType: normalizeReportText(record.inStockType),
+ ownerTypeId: normalizeReportText(record.ownerTypeId),
+ ownerId: normalizeReportText(record.ownerId),
+ ownerName: normalizeReportText(record.ownerName),
+ keeperTypeId: normalizeReportText(record.keeperTypeId),
+ keeperId: normalizeReportText(record.keeperId),
+ keeperName: normalizeReportText(record.keeperName),
+ targetWarehouseId: normalizeReportText(record.targetWarehouseId),
+ sourceWarehouseId: normalizeReportText(record.sourceWarehouseId),
+ createByText: record['createBy$'] || record.createByText || '--',
+ createTimeText: record['createTime$'] || record.createTime || '--',
+ updateByText: record['updateBy$'] || record.updateByText || '--',
+ updateTimeText: record['updateTime$'] || record.updateTime || '--',
+ memo: normalizeReportText(record.memo),
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type
+ }
+}
+
+export function getTaskItemLogReportColumns() {
+ return [
+ { source: 'taskId', label: '浠诲姟ID' },
+ { source: 'taskItemId', label: '浠诲姟鏄庣粏ID' },
+ { source: 'matnrId', label: '鐗╂枡ID' },
+ { source: 'maktx', label: '鐗╂枡鍚嶇О' },
+ { source: 'platWorkCode', label: '宸ュ崟鍙�' },
+ { source: 'platOrderCode', label: '瀹㈡埛璁㈠崟鍙�' },
+ { source: 'projectCode', label: '椤圭洰鍙�' },
+ { source: 'source', label: '婧愮紪鐮�' },
+ { source: 'sourceCode', label: '婧愬崟鍙�' },
+ { source: 'matnrCode', label: '鐗╂枡缂栫爜' },
+ { source: 'unit', label: '搴撳瓨鍗曚綅' },
+ { source: 'anfme', label: '鏁伴噺' },
+ { source: 'batch', label: '搴撳瓨鎵规' },
+ { source: 'spec', label: '瑙勬牸' },
+ { source: 'model', label: '鍨嬪彿' },
+ { source: 'fieldsIndex', label: '瀛楁绱㈠紩' },
+ { source: 'createByText', label: '鍒涘缓浜�' },
+ { source: 'createTimeText', label: '鍒涘缓鏃堕棿' },
+ { source: 'updateByText', label: '鏇存柊浜�' },
+ { source: 'updateTimeText', label: '鏇存柊鏃堕棿' },
+ { source: 'statusText', label: '鐘舵��' },
+ { source: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildTaskItemLogPrintRows(records = []) {
+ return records.map((record) => {
+ const row = normalizeTaskItemLogRow(record)
+ return {
+ taskId: row.taskId,
+ taskItemId: row.taskItemId,
+ matnrId: row.matnrId,
+ maktx: row.maktx,
+ platWorkCode: row.platWorkCode,
+ platOrderCode: row.platOrderCode,
+ projectCode: row.projectCode,
+ source: row.source,
+ sourceCode: row.sourceCode,
+ matnrCode: row.matnrCode,
+ unit: row.unit,
+ anfme: row.anfme,
+ batch: row.batch,
+ spec: row.spec,
+ model: row.model,
+ fieldsIndex: row.fieldsIndex,
+ createByText: row.createByText,
+ createTimeText: row.createTimeText,
+ updateByText: row.updateByText,
+ updateTimeText: row.updateTimeText,
+ statusText: row.statusText,
+ statusType: row.statusType,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/manager/task-item-log/taskItemLogTable.columns.js b/rsf-design/src/views/manager/task-item-log/taskItemLogTable.columns.js
new file mode 100644
index 0000000..c358b4e
--- /dev/null
+++ b/rsf-design/src/views/manager/task-item-log/taskItemLogTable.columns.js
@@ -0,0 +1,164 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createTaskItemLogTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'taskId',
+ label: '浠诲姟ID',
+ width: 110,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'taskItemId',
+ label: '浠诲姟鏄庣粏ID',
+ width: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrId',
+ label: '鐗╂枡ID',
+ width: 110,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platWorkCode',
+ label: '宸ュ崟鍙�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platOrderCode',
+ label: '瀹㈡埛璁㈠崟鍙�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'projectCode',
+ label: '椤圭洰鍙�',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'source',
+ label: '婧愮紪鐮�',
+ width: 110,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'sourceCode',
+ label: '婧愬崟鍙�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '搴撳瓨鍗曚綅',
+ width: 100,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right',
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '搴撳瓨鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'spec',
+ label: '瑙勬牸',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'model',
+ label: '鍨嬪彿',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '瀛楁绱㈠紩',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.statusType || 'info',
+ effect: 'light'
+ },
+ () => row?.statusText || '--'
+ )
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/task-item/index.vue b/rsf-design/src/views/manager/task-item/index.vue
new file mode 100644
index 0000000..4d41189
--- /dev/null
+++ b/rsf-design/src/views/manager/task-item/index.vue
@@ -0,0 +1,265 @@
+<template>
+ <div class="task-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <TaskItemDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useRoute } from 'vue-router'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ fetchExportTaskItemReport,
+ fetchGetTaskItemDetail,
+ fetchGetTaskItemMany,
+ fetchTaskItemPage
+ } from '@/api/task-item'
+ import {
+ buildTaskItemPageQueryParams,
+ buildTaskItemPrintRows,
+ buildTaskItemSearchParams,
+ createTaskItemSearchState,
+ getTaskItemPaginationKey,
+ getTaskItemReportColumns,
+ normalizeTaskItemRow,
+ TASK_ITEM_REPORT_TITLE
+ } from './taskItemPage.helpers'
+ import { createTaskItemTableColumns } from './taskItemTable.columns'
+ import TaskItemDetailDrawer from './modules/task-item-detail-drawer.vue'
+
+ defineOptions({ name: 'TaskItem' })
+
+ const route = useRoute()
+ const userStore = useUserStore()
+ const initialTaskId = route.query.taskId ? Number(route.query.taskId) : ''
+ const searchForm = ref(createTaskItemSearchState({ taskId: Number.isFinite(initialTaskId) ? initialTaskId : '' }))
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = TASK_ITEM_REPORT_TITLE
+ const reportQueryParams = computed(() => buildTaskItemSearchParams(searchForm.value))
+ const reportColumns = getTaskItemReportColumns()
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ伐鍗曞彿/瀹㈡埛璁㈠崟鍙�/鐗╂枡缂栫爜'
+ }
+ },
+ {
+ label: '浠诲姟ID',
+ key: 'taskId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ换鍔D'
+ }
+ },
+ {
+ label: '涓诲崟ID',
+ key: 'orderId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ富鍗旾D'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '宸ュ崟鍙�',
+ key: 'platWorkCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ伐鍗曞彿'
+ }
+ },
+ {
+ label: '瀹㈡埛璁㈠崟鍙�',
+ key: 'platOrderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鎴疯鍗曞彿'
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchTaskItemPage,
+ apiParams: buildTaskItemPageQueryParams(searchForm.value),
+ paginationKey: getTaskItemPaginationKey(),
+ columnsFactory: () => createTaskItemTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeTaskItemRow(item)) : [])
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetTaskItemMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchTaskItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'task-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportTaskItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTaskItemPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetTaskItemDetail(id)
+ detailData.value = normalizeTaskItemRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTaskItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ const resetSeed = route.query.taskId ? { taskId: Number(route.query.taskId) || '' } : {}
+ Object.assign(searchForm.value, createTaskItemSearchState(resetSeed))
+ resetSearchParams(buildTaskItemPageQueryParams(createTaskItemSearchState(resetSeed)))
+ }
+</script>
diff --git a/rsf-design/src/views/manager/task-item/modules/task-item-detail-drawer.vue b/rsf-design/src/views/manager/task-item/modules/task-item-detail-drawer.vue
new file mode 100644
index 0000000..1629b82
--- /dev/null
+++ b/rsf-design/src/views/manager/task-item/modules/task-item-detail-drawer.vue
@@ -0,0 +1,56 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟妗f槑缁嗚鎯�"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="浠诲姟ID">{{ detail.taskId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓诲崟ID">{{ detail.orderId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detail.orderTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁鏄庣粏ID">{{ detail.orderItemId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡ID">{{ detail.matnrId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ュ崟鍙�">{{ detail.platWorkCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛璁㈠崟鍙�">{{ detail.platOrderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰鍙�">{{ detail.projectCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹屾垚鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勬牸">{{ detail.spec || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍨嬪彿">{{ detail.model || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'TaskItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/task-item/taskItemPage.helpers.js b/rsf-design/src/views/manager/task-item/taskItemPage.helpers.js
new file mode 100644
index 0000000..941d1b4
--- /dev/null
+++ b/rsf-design/src/views/manager/task-item/taskItemPage.helpers.js
@@ -0,0 +1,203 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'info', bool: false }
+}
+
+export const TASK_ITEM_REPORT_TITLE = '浠诲姟妗f槑缁嗘姤琛�'
+export const TASK_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+export function createTaskItemSearchState(seed = {}) {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ taskId: '',
+ orderId: '',
+ orderType: '',
+ orderItemId: '',
+ matnrId: '',
+ maktx: '',
+ matnrCode: '',
+ unit: '',
+ anfme: '',
+ platOrderCode: '',
+ platWorkCode: '',
+ projectCode: '',
+ batch: '',
+ spec: '',
+ model: '',
+ memo: '',
+ status: '',
+ ...seed
+ }
+}
+
+export function getTaskItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildTaskItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'maktx',
+ 'matnrCode',
+ 'unit',
+ 'platOrderCode',
+ 'platWorkCode',
+ 'projectCode',
+ 'batch',
+ 'spec',
+ 'model',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;['taskId', 'orderId', 'orderType', 'orderItemId', 'matnrId', 'anfme', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildTaskItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTaskItemSearchParams(params)
+ }
+}
+
+function getTaskItemStatusMeta(value) {
+ return STATUS_META[Number(value)] || { text: '-', type: 'info', bool: false }
+}
+
+export function normalizeTaskItemRow(record = {}) {
+ const statusMeta = getTaskItemStatusMeta(record.status)
+ return {
+ ...record,
+ taskId: record.taskId ?? '--',
+ orderId: record.orderId ?? '--',
+ orderTypeText: record['orderType$'] || record.orderTypeText || record.orderType || '--',
+ wkTypeText: record['wkType$'] || record.wkTypeText || record.wkType || '--',
+ orderItemId: record.orderItemId ?? '--',
+ matnrId: record.matnrId ?? '--',
+ matnrCode: normalizeText(record.matnrCode) || '--',
+ maktx: normalizeText(record.maktx) || '--',
+ platItemId: normalizeText(record.platItemId) || '--',
+ platOrderCode: normalizeText(record.platOrderCode) || '--',
+ platWorkCode: normalizeText(record.platWorkCode) || '--',
+ projectCode: normalizeText(record.projectCode) || '--',
+ batch: normalizeText(record.batch) || '--',
+ unit: normalizeText(record.unit) || '--',
+ anfme: record.anfme ?? '--',
+ workQty: record.workQty ?? '--',
+ qty: record.qty ?? '--',
+ spec: normalizeText(record.spec) || '--',
+ model: normalizeText(record.model) || '--',
+ fieldsIndex: normalizeText(record.fieldsIndex) || '--',
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function getTaskItemReportColumns() {
+ return [
+ { prop: 'taskId', label: '浠诲姟ID' },
+ { prop: 'orderId', label: '涓诲崟ID' },
+ { prop: 'orderTypeText', label: '鍗曟嵁绫诲瀷' },
+ { prop: 'orderItemId', label: '鍗曟嵁鏄庣粏ID' },
+ { prop: 'matnrCode', label: '鐗╂枡缂栫爜' },
+ { prop: 'maktx', label: '鐗╂枡鍚嶇О' },
+ { prop: 'platWorkCode', label: '宸ュ崟鍙�' },
+ { prop: 'platOrderCode', label: '瀹㈡埛璁㈠崟鍙�' },
+ { prop: 'projectCode', label: '椤圭洰鍙�' },
+ { prop: 'batch', label: '搴撳瓨鎵规' },
+ { prop: 'unit', label: '搴撳瓨鍗曚綅' },
+ { prop: 'anfme', label: '鏁伴噺' },
+ { prop: 'workQty', label: '鎵ц鏁伴噺' },
+ { prop: 'qty', label: '瀹屾垚鏁伴噺' },
+ { prop: 'spec', label: '瑙勬牸' },
+ { prop: 'model', label: '鍨嬪彿' },
+ { prop: 'createByText', label: '鍒涘缓浜�' },
+ { prop: 'createTimeText', label: '鍒涘缓鏃堕棿' },
+ { prop: 'updateByText', label: '鏇存柊浜�' },
+ { prop: 'updateTimeText', label: '鏇存柊鏃堕棿' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildTaskItemPrintRows(records = []) {
+ return (Array.isArray(records) ? records : []).map((record) => {
+ const row = normalizeTaskItemRow(record)
+ return {
+ taskId: row.taskId,
+ orderId: row.orderId,
+ orderTypeText: row.orderTypeText,
+ orderItemId: row.orderItemId,
+ matnrCode: row.matnrCode,
+ maktx: row.maktx,
+ platWorkCode: row.platWorkCode,
+ platOrderCode: row.platOrderCode,
+ projectCode: row.projectCode,
+ batch: row.batch,
+ unit: row.unit,
+ anfme: row.anfme,
+ workQty: row.workQty,
+ qty: row.qty,
+ spec: row.spec,
+ model: row.model,
+ createByText: row.createByText,
+ createTimeText: row.createTimeText,
+ updateByText: row.updateByText,
+ updateTimeText: row.updateTimeText,
+ statusText: row.statusText,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/manager/task-item/taskItemTable.columns.js b/rsf-design/src/views/manager/task-item/taskItemTable.columns.js
new file mode 100644
index 0000000..b7f82b3
--- /dev/null
+++ b/rsf-design/src/views/manager/task-item/taskItemTable.columns.js
@@ -0,0 +1,109 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createTaskItemTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'taskId',
+ label: '浠诲姟ID',
+ minWidth: 110,
+ align: 'right'
+ },
+ {
+ prop: 'orderId',
+ label: '涓诲崟ID',
+ minWidth: 110,
+ align: 'right'
+ },
+ {
+ prop: 'orderTypeText',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 110,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orderItemId',
+ label: '鍗曟嵁鏄庣粏ID',
+ minWidth: 120,
+ align: 'right'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platWorkCode',
+ label: '宸ュ崟鍙�',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '搴撳瓨鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '搴撳瓨鍗曚綅',
+ width: 100,
+ align: 'center'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.statusType || 'info',
+ effect: 'light'
+ },
+ () => row?.statusText || '--'
+ )
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/task-log/index.vue b/rsf-design/src/views/manager/task-log/index.vue
new file mode 100644
index 0000000..eb0a2b2
--- /dev/null
+++ b/rsf-design/src/views/manager/task-log/index.vue
@@ -0,0 +1,250 @@
+<template>
+ <div class="task-log-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <TaskLogDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ fetchExportTaskLogReport,
+ fetchGetTaskLogDetail,
+ fetchGetTaskLogMany,
+ fetchTaskLogPage
+ } from '@/api/task-log'
+ import {
+ buildTaskLogPageQueryParams,
+ buildTaskLogPrintRows,
+ buildTaskLogSearchParams,
+ createTaskLogSearchState,
+ getTaskLogPaginationKey,
+ getTaskLogReportColumns,
+ normalizeTaskLogRow,
+ TASK_LOG_REPORT_TITLE
+ } from './taskLogPage.helpers'
+ import { createTaskLogTableColumns } from './taskLogTable.columns'
+ import TaskLogDetailDrawer from './modules/task-log-detail-drawer.vue'
+
+ defineOptions({ name: 'TaskLog' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createTaskLogSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = TASK_LOG_REPORT_TITLE
+ const reportQueryParams = computed(() => buildTaskLogSearchParams(searchForm.value))
+ const reportColumns = getTaskLogReportColumns()
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ换鍔″彿/鎵樼洏鐮�/鏈哄櫒浜虹紪鐮�'
+ }
+ },
+ {
+ label: '浠诲姟鍙�',
+ key: 'taskCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ换鍔″彿'
+ }
+ },
+ {
+ label: '婧愬簱浣�',
+ key: 'orgLoc',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ簮搴撲綅'
+ }
+ },
+ {
+ label: '鐩爣搴撲綅',
+ key: 'targLoc',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ洰鏍囧簱浣�'
+ }
+ },
+ {
+ label: '鎵樼洏鐮�',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ墭鐩樼爜'
+ }
+ },
+ {
+ label: '鏈哄櫒浜虹紪鐮�',
+ key: 'robotCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ満鍣ㄤ汉缂栫爜'
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchTaskLogPage,
+ apiParams: buildTaskLogPageQueryParams(searchForm.value),
+ paginationKey: getTaskLogPaginationKey(),
+ columnsFactory: () => createTaskLogTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeTaskLogRow(item)) : [])
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetTaskLogMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchTaskLogPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'task-log.xlsx',
+ requestExport: (payload) =>
+ fetchExportTaskLogReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTaskLogPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetTaskLogDetail(id)
+ detailData.value = normalizeTaskLogRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTaskLogSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTaskLogSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/manager/task-log/modules/task-log-detail-drawer.vue b/rsf-design/src/views/manager/task-log/modules/task-log-detail-drawer.vue
new file mode 100644
index 0000000..b2a5166
--- /dev/null
+++ b/rsf-design/src/views/manager/task-log/modules/task-log-detail-drawer.vue
@@ -0,0 +1,54 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟鍘嗗彶妗h鎯�"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="浠诲姟ID">{{ detail.taskId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鍙�">{{ detail.taskCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鐘舵��">{{ detail.taskStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟绫诲瀷">{{ detail.taskTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愬簱浣�">{{ detail.orgLoc || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愮珯鐐�">{{ detail.orgSite || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣搴撲綅">{{ detail.targLoc || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣绔欑偣">{{ detail.targSite || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈哄櫒浜虹紪鐮�">{{ detail.robotCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鐘舵��">{{ detail.exceStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浼樺厛绾�">{{ detail.sort ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寮傚父鎻忚堪">{{ detail.expDesc || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寮傚父缂栫爜">{{ detail.expCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寮�濮嬫椂闂�">{{ detail.startTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁撴潫鏃堕棿">{{ detail.endTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'TaskLogDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/task-log/taskLogPage.helpers.js b/rsf-design/src/views/manager/task-log/taskLogPage.helpers.js
new file mode 100644
index 0000000..41c7013
--- /dev/null
+++ b/rsf-design/src/views/manager/task-log/taskLogPage.helpers.js
@@ -0,0 +1,162 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const TASK_LOG_REPORT_TITLE = '浠诲姟鍘嗗彶妗f姤琛�'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+export function createTaskLogSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ taskId: '',
+ taskCode: '',
+ taskStatus: '',
+ taskType: '',
+ orgLoc: '',
+ orgSite: '',
+ targLoc: '',
+ targSite: '',
+ barcode: '',
+ robotCode: '',
+ exceStatus: '',
+ expDesc: '',
+ sort: '',
+ expCode: '',
+ startTime: '',
+ endTime: ''
+ }
+}
+
+export function getTaskLogPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildTaskLogSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'taskCode',
+ 'orgLoc',
+ 'orgSite',
+ 'targLoc',
+ 'targSite',
+ 'barcode',
+ 'robotCode',
+ 'expDesc',
+ 'expCode'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd', 'startTime', 'endTime'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;['taskId', 'taskStatus', 'taskType', 'exceStatus', 'sort'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildTaskLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTaskLogSearchParams(params)
+ }
+}
+
+export function getTaskLogStatusMeta(value) {
+ return STATUS_META[Number(value)] || { text: '-', type: 'info' }
+}
+
+export function normalizeTaskLogRow(record = {}) {
+ const statusMeta = getTaskLogStatusMeta(record.status)
+ return {
+ ...record,
+ taskId: record.taskId ?? '--',
+ taskCode: record.taskCode || '--',
+ taskStatusText: record['taskStatus$'] || record.taskStatusText || (record.taskStatus ?? '--'),
+ taskTypeText: record['taskType$'] || record.taskTypeText || (record.taskType ?? '--'),
+ orgLoc: record.orgLoc || '--',
+ orgSite: record.orgSite || '--',
+ targLoc: record.targLoc || '--',
+ targSite: record.targSite || '--',
+ barcode: record.barcode || '--',
+ robotCode: record.robotCode || '--',
+ exceStatusText: record.exceStatusText || (record.exceStatus ?? '--'),
+ expDesc: record.expDesc || '--',
+ sort: record.sort ?? '--',
+ expCode: record.expCode || '--',
+ startTimeText: record['startTime$'] || record.startTime || '--',
+ endTimeText: record['endTime$'] || record.endTime || '--',
+ createByText: record['createBy$'] || record.createByText || '--',
+ createTimeText: record['createTime$'] || record.createTime || '--',
+ updateByText: record['updateBy$'] || record.updateByText || '--',
+ updateTimeText: record['updateTime$'] || record.updateTime || '--',
+ memo: record.memo || '--',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type
+ }
+}
+
+export function getTaskLogReportColumns() {
+ return [
+ { prop: 'taskCode', label: '浠诲姟鍙�' },
+ { prop: 'taskStatusText', label: '浠诲姟鐘舵��' },
+ { prop: 'taskTypeText', label: '浠诲姟绫诲瀷' },
+ { prop: 'orgLoc', label: '婧愬簱浣�' },
+ { prop: 'targLoc', label: '鐩爣搴撲綅' },
+ { prop: 'barcode', label: '鎵樼洏鐮�' },
+ { prop: 'robotCode', label: '鏈哄櫒浜虹紪鐮�' },
+ { prop: 'startTimeText', label: '寮�濮嬫椂闂�' },
+ { prop: 'endTimeText', label: '缁撴潫鏃堕棿' }
+ ]
+}
+
+export function buildTaskLogPrintRows(records = []) {
+ return records.map((record) => {
+ const row = normalizeTaskLogRow(record)
+ return {
+ taskCode: row.taskCode,
+ taskStatusText: row.taskStatusText,
+ taskTypeText: row.taskTypeText,
+ orgLoc: row.orgLoc,
+ targLoc: row.targLoc,
+ barcode: row.barcode,
+ robotCode: row.robotCode,
+ startTimeText: row.startTimeText,
+ endTimeText: row.endTimeText
+ }
+ })
+}
diff --git a/rsf-design/src/views/manager/task-log/taskLogTable.columns.js b/rsf-design/src/views/manager/task-log/taskLogTable.columns.js
new file mode 100644
index 0000000..2ece020
--- /dev/null
+++ b/rsf-design/src/views/manager/task-log/taskLogTable.columns.js
@@ -0,0 +1,109 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createTaskLogTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'taskCode',
+ label: '浠诲姟鍙�',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'taskStatusText',
+ label: '浠诲姟鐘舵��',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'taskTypeText',
+ label: '浠诲姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orgLoc',
+ label: '婧愬簱浣�',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orgSite',
+ label: '婧愮珯鐐�',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'targLoc',
+ label: '鐩爣搴撲綅',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'targSite',
+ label: '鐩爣绔欑偣',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'barcode',
+ label: '鎵樼洏鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'robotCode',
+ label: '鏈哄櫒浜虹紪鐮�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'startTimeText',
+ label: '寮�濮嬫椂闂�',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'endTimeText',
+ label: '缁撴潫鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.statusType || 'info',
+ effect: 'light'
+ },
+ () => row?.statusText || '--'
+ )
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/task/index.vue b/rsf-design/src/views/manager/task/index.vue
new file mode 100644
index 0000000..138697d
--- /dev/null
+++ b/rsf-design/src/views/manager/task/index.vue
@@ -0,0 +1,352 @@
+<template>
+ <div class="task-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <TaskDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :data="detailTableData"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @refresh="loadDetailResources"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchCheckTask,
+ fetchCompleteTask,
+ fetchPickTask,
+ fetchRemoveTask,
+ fetchTaskDetail,
+ fetchTaskItemPage,
+ fetchTaskPage,
+ fetchTopTask
+ } from '@/api/task'
+ import TaskDetailDrawer from './modules/task-detail-drawer.vue'
+ import { createTaskTableColumns } from './taskTable.columns'
+ import {
+ buildTaskPageQueryParams,
+ confirmTaskAction,
+ createTaskSearchState,
+ normalizeTaskItemRow,
+ normalizeTaskRow
+ } from './taskPage.helpers'
+
+ defineOptions({ name: 'Task' })
+
+ const loading = ref(false)
+ const tableData = ref([])
+ const searchForm = ref(createTaskSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const detailTableData = ref([])
+ const activeTaskRow = ref(null)
+
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヤ换鍔″彿/搴撲綅/鎵樼洏鐮�' }
+ },
+ {
+ label: '浠诲姟鍙�',
+ key: 'taskCode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヤ换鍔″彿' }
+ },
+ {
+ label: '婧愬簱浣�',
+ key: 'orgLoc',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ簮搴撲綅' }
+ },
+ {
+ label: '鐩爣搴撲綅',
+ key: 'targLoc',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ョ洰鏍囧簱浣�' }
+ },
+ {
+ label: '鎵樼洏鐮�',
+ key: 'barcode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ墭鐩樼爜' }
+ }
+ ])
+
+ const detailColumns = computed(() => [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'orderTypeLabel',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platWorkCode',
+ label: '宸ュ崟鍙�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platItemId',
+ label: '琛屽彿',
+ minWidth: 100,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ }
+ ])
+
+ async function openDetailDrawer(row) {
+ activeTaskRow.value = row
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ await loadDetailResources()
+ }
+
+ async function handleActionClick(action, row) {
+ try {
+ if (action.key === 'view') {
+ await openDetailDrawer(row)
+ return
+ }
+
+ if (action.key === 'complete') {
+ await confirmTaskAction(`纭畾瀹屾垚浠诲姟 ${row.taskCode || ''} 鍚楋紵`)
+ await fetchCompleteTask(row.id)
+ ElMessage.success('浠诲姟瀹屾垚鎴愬姛')
+ } else if (action.key === 'remove') {
+ await confirmTaskAction(`纭畾鍙栨秷浠诲姟 ${row.taskCode || ''} 鍚楋紵`)
+ await fetchRemoveTask(row.id)
+ ElMessage.success('浠诲姟鍙栨秷鎴愬姛')
+ } else if (action.key === 'check') {
+ await confirmTaskAction(`纭畾鎵ц鐩樼偣鍑哄簱浠诲姟 ${row.taskCode || ''} 鍚楋紵`)
+ await fetchCheckTask(row.id)
+ ElMessage.success('鐩樼偣鍑哄簱鎴愬姛')
+ } else if (action.key === 'pick') {
+ await confirmTaskAction(`纭畾鎵ц鎷f枡鍑哄簱浠诲姟 ${row.taskCode || ''} 鍚楋紵`)
+ await fetchPickTask(row.id)
+ ElMessage.success('鎷f枡鍑哄簱鎴愬姛')
+ } else if (action.key === 'top') {
+ await fetchTopTask(row.id)
+ ElMessage.success('浠诲姟缃《鎴愬姛')
+ }
+
+ await loadPageData()
+ if (detailDrawerVisible.value && activeTaskRow.value?.id === row.id) {
+ await loadDetailResources()
+ }
+ } catch (error) {
+ if (error === 'cancel') {
+ return
+ }
+ ElMessage.error(error?.message || '浠诲姟鎿嶄綔澶辫触')
+ }
+ }
+
+ const { columns, columnChecks } = useTableColumns(() => createTaskTableColumns(handleActionClick))
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchTaskPage(
+ buildTaskPageQueryParams({
+ ...searchForm.value,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: pagination.current,
+ size: pagination.size
+ },
+ { timeoutMessage: '浠诲姟鍒楄〃鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeTaskRow(record))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function loadDetailResources() {
+ if (!activeTaskRow.value?.id) {
+ return
+ }
+
+ detailLoading.value = true
+ try {
+ const [detailResponse, taskItemResponse] = await Promise.all([
+ guardRequestWithMessage(fetchTaskDetail(activeTaskRow.value.id), {}, {
+ timeoutMessage: '浠诲姟璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }),
+ guardRequestWithMessage(
+ fetchTaskItemPage({
+ taskId: activeTaskRow.value.id,
+ current: detailPagination.current,
+ pageSize: detailPagination.size
+ }),
+ {
+ records: [],
+ total: 0,
+ current: detailPagination.current,
+ size: detailPagination.size
+ },
+ { timeoutMessage: '浠诲姟鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ ])
+
+ detailData.value = normalizeTaskRow(detailResponse)
+ detailTableData.value = Array.isArray(taskItemResponse?.records)
+ ? taskItemResponse.records.map((record) => normalizeTaskItemRow(record))
+ : []
+ updatePaginationState(detailPagination, taskItemResponse, detailPagination.current, detailPagination.size)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ searchForm.value = createTaskSearchState()
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ detailPagination.current = 1
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ onMounted(loadPageData)
+</script>
diff --git a/rsf-design/src/views/manager/task/modules/task-detail-drawer.vue b/rsf-design/src/views/manager/task/modules/task-detail-drawer.vue
new file mode 100644
index 0000000..7a65cb6
--- /dev/null
+++ b/rsf-design/src/views/manager/task/modules/task-detail-drawer.vue
@@ -0,0 +1,59 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟璇︽儏"
+ size="85%"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="浠诲姟鍙�">{{ detail.taskCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鐘舵��">{{ detail.taskStatusLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟绫诲瀷">{{ detail.taskTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁惧绫诲瀷">{{ detail.warehTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愬簱浣�">{{ detail.orgLoc || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愮珯鐐�">{{ detail.orgSiteLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣搴撲綅">{{ detail.targLoc || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣绔欑偣">{{ detail.targSiteLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈哄櫒浜虹紪鐮�">{{ detail.robotCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浼樺厛绾�">{{ detail.sort ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="flex items-center justify-between">
+ <div class="text-sm text-[var(--art-gray-600)]">浠诲姟鏄庣粏</div>
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/task/taskPage.helpers.js b/rsf-design/src/views/manager/task/taskPage.helpers.js
new file mode 100644
index 0000000..0fa5bda
--- /dev/null
+++ b/rsf-design/src/views/manager/task/taskPage.helpers.js
@@ -0,0 +1,146 @@
+import { ElMessageBox } from 'element-plus'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+export function createTaskSearchState() {
+ return {
+ condition: '',
+ taskCode: '',
+ orgLoc: '',
+ targLoc: '',
+ barcode: ''
+ }
+}
+
+export function buildTaskPageQueryParams(params = {}) {
+ const result = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+
+ ;['condition', 'taskCode', 'orgLoc', 'targLoc', 'barcode'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function normalizeTaskRow(record = {}) {
+ return {
+ ...record,
+ taskCode: record.taskCode || '-',
+ taskStatusLabel: record['taskStatus$'] || '-',
+ taskTypeLabel: record['taskType$'] || '-',
+ warehTypeLabel: record['warehType$'] || '-',
+ orgLoc: record.orgLoc || '-',
+ orgSiteLabel: record['orgSite$'] || record.orgSite || '-',
+ targLoc: record.targLoc || '-',
+ targSiteLabel: record['targSite$'] || record.targSite || '-',
+ barcode: record.barcode || '-',
+ robotCode: record.robotCode || '-',
+ sort: normalizeNumber(record.sort),
+ statusText: record['status$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ canComplete: record.canComplete === true
+ }
+}
+
+export function normalizeTaskItemRow(record = {}) {
+ return {
+ ...record,
+ orderTypeLabel: record['orderType$'] || '-',
+ wkTypeLabel: record['wkType$'] || '-',
+ platWorkCode: record.platWorkCode || '-',
+ platItemId: record.platItemId || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ batch: record.batch || '-',
+ unit: record.unit || '-',
+ anfme: normalizeNumber(record.anfme),
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-'
+ }
+}
+
+export function canCheckTask(row = {}) {
+ return Number(row.taskStatus) === 199 && Number(row.taskType) === 107
+}
+
+export function canPickTask(row = {}) {
+ return Number(row.taskStatus) === 199 && Number(row.taskType) === 103
+}
+
+export function getTaskActionList(row = {}) {
+ return [
+ {
+ key: 'view',
+ label: '鏌ョ湅璇︽儏',
+ icon: 'ri:eye-line'
+ },
+ ...(row.canComplete
+ ? [
+ {
+ key: 'complete',
+ label: '瀹屾垚浠诲姟',
+ icon: 'ri:checkbox-circle-line',
+ auth: 'update'
+ }
+ ]
+ : []),
+ ...(canCheckTask(row)
+ ? [
+ {
+ key: 'check',
+ label: '鐩樼偣鍑哄簱',
+ icon: 'ri:file-check-line',
+ auth: 'update'
+ }
+ ]
+ : []),
+ ...(canPickTask(row)
+ ? [
+ {
+ key: 'pick',
+ label: '鎷f枡鍑哄簱',
+ icon: 'ri:paint-line',
+ auth: 'update'
+ }
+ ]
+ : []),
+ {
+ key: 'top',
+ label: '浠诲姟缃《',
+ icon: 'ri:pushpin-line',
+ auth: 'update'
+ },
+ {
+ key: 'remove',
+ label: '鍙栨秷浠诲姟',
+ icon: 'ri:close-circle-line',
+ color: '#f56c6c',
+ auth: 'delete'
+ }
+ ]
+}
+
+export async function confirmTaskAction(message) {
+ await ElMessageBox.confirm(message, '鎻愮ず', {
+ type: 'warning',
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷'
+ })
+}
diff --git a/rsf-design/src/views/manager/task/taskTable.columns.js b/rsf-design/src/views/manager/task/taskTable.columns.js
new file mode 100644
index 0000000..63b5da5
--- /dev/null
+++ b/rsf-design/src/views/manager/task/taskTable.columns.js
@@ -0,0 +1,93 @@
+import { h } from 'vue'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getTaskActionList } from './taskPage.helpers'
+
+export function createTaskTableColumns(handleActionClick) {
+ return [
+ {
+ prop: 'taskCode',
+ label: '浠诲姟鍙�',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'taskStatusLabel',
+ label: '浠诲姟鐘舵��',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'taskTypeLabel',
+ label: '浠诲姟绫诲瀷',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'warehTypeLabel',
+ label: '璁惧绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orgLoc',
+ label: '婧愬簱浣�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orgSiteLabel',
+ label: '婧愮珯鐐�',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'targLoc',
+ label: '鐩爣搴撲綅',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'targSiteLabel',
+ label: '鐩爣绔欑偣',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'barcode',
+ label: '鎵樼洏鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'robotCode',
+ label: '鏈哄櫒浜虹紪鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'sort',
+ label: '浼樺厛绾�',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 120,
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', [
+ h(ArtButtonMore, {
+ list: getTaskActionList(row),
+ onClick: (item) => handleActionClick(item, row)
+ })
+ ])
+ }
+ ]
+}
diff --git a/rsf-design/src/views/manager/wave-rule/index.vue b/rsf-design/src/views/manager/wave-rule/index.vue
new file mode 100644
index 0000000..41970b2
--- /dev/null
+++ b/rsf-design/src/views/manager/wave-rule/index.vue
@@ -0,0 +1,248 @@
+<template>
+ <div class="wave-rule-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板娉㈡绛栫暐</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <WaveRuleDialog
+ v-model:visible="dialogVisible"
+ :wave-rule-data="currentWaveRuleData"
+ :type-options="typeOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <WaveRuleDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import {
+ fetchDeleteWaveRule,
+ fetchDictDataPage,
+ fetchGetWaveRuleDetail,
+ fetchSaveWaveRule,
+ fetchUpdateWaveRule,
+ fetchWaveRulePage
+ } from '@/api/system-manage'
+ import WaveRuleDialog from './modules/wave-rule-dialog.vue'
+ import WaveRuleDetailDrawer from './modules/wave-rule-detail-drawer.vue'
+ import { createWaveRuleTableColumns } from './waveRuleTable.columns'
+ import {
+ buildWaveRuleDialogModel,
+ buildWaveRulePageQueryParams,
+ buildWaveRuleSavePayload,
+ buildWaveRuleSearchParams,
+ buildWaveRuleTypeOptions,
+ createWaveRuleSearchState,
+ getWaveRuleDictTypeCode,
+ getWaveRulePaginationKey,
+ normalizeWaveRuleListRow
+ } from './waveRulePage.helpers'
+
+ defineOptions({ name: 'WaveRule' })
+
+ const { hasAuth } = useAuth()
+ const searchForm = ref(createWaveRuleSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const typeOptions = ref([])
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鍙锋垨鍚嶇О'
+ }
+ },
+ {
+ label: '缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鍙�'
+ }
+ },
+ {
+ label: '绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: typeOptions.value
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ瓥鐣ュ悕绉�'
+ }
+ }
+ ])
+
+ async function loadTypeOptions() {
+ const records = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: getWaveRuleDictTypeCode(),
+ status: 1
+ }),
+ [],
+ {
+ timeoutMessage: '娉㈡绛栫暐绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ typeOptions.value = buildWaveRuleTypeOptions(Array.isArray(records?.records) ? records.records : records?.list || records || [])
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeWaveRuleListRow(await fetchGetWaveRuleDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇娉㈡绛栫暐璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentWaveRuleData.value = buildWaveRuleDialogModel(await fetchGetWaveRuleDetail(row.id))
+ dialogVisible.value = true
+ dialogType.value = 'edit'
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇娉㈡绛栫暐璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchWaveRulePage,
+ apiParams: buildWaveRulePageQueryParams(searchForm.value),
+ paginationKey: getWaveRulePaginationKey(),
+ columnsFactory: () =>
+ createWaveRuleTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeWaveRuleListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentWaveRuleData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildWaveRuleDialogModel(),
+ buildEditModel: (record) => buildWaveRuleDialogModel(record),
+ buildSavePayload: (formData) => buildWaveRuleSavePayload(formData),
+ saveRequest: fetchSaveWaveRule,
+ updateRequest: fetchUpdateWaveRule,
+ deleteRequest: fetchDeleteWaveRule,
+ entityName: '娉㈡绛栫暐',
+ resolveRecordLabel: (record) => record?.name || record?.code || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ onMounted(() => {
+ loadTypeOptions()
+ })
+
+ function handleSearch(params) {
+ replaceSearchParams(buildWaveRuleSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createWaveRuleSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/manager/wave-rule/modules/wave-rule-detail-drawer.vue b/rsf-design/src/views/manager/wave-rule/modules/wave-rule-detail-drawer.vue
new file mode 100644
index 0000000..1e4247e
--- /dev/null
+++ b/rsf-design/src/views/manager/wave-rule/modules/wave-rule-detail-drawer.vue
@@ -0,0 +1,41 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="娉㈡绛栫暐璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="缂栧彿">{{ displayData.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绫诲瀷">{{ displayData.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚嶇О">{{ displayData.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ displayData.updateByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeWaveRuleListRow } from '../waveRulePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeWaveRuleListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/manager/wave-rule/modules/wave-rule-dialog.vue b/rsf-design/src/views/manager/wave-rule/modules/wave-rule-dialog.vue
new file mode 100644
index 0000000..5de7261
--- /dev/null
+++ b/rsf-design/src/views/manager/wave-rule/modules/wave-rule-dialog.vue
@@ -0,0 +1,153 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildWaveRuleDialogModel, createWaveRuleFormState } from '../waveRulePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ waveRuleData: { type: Object, default: () => ({}) },
+ typeOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createWaveRuleFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫娉㈡绛栫暐' : '鏂板娉㈡绛栫暐'))
+
+ const rules = computed(() => ({
+ type: [{ required: true, message: '璇烽�夋嫨绛栫暐绫诲瀷', trigger: 'change' }],
+ name: [{ required: true, message: '璇疯緭鍏ョ瓥鐣ュ悕绉�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ disabled: true,
+ placeholder: '鏂板鍚庤嚜鍔ㄧ敓鎴�'
+ }
+ },
+ {
+ label: '绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ options: props.typeOptions,
+ placeholder: '璇烽�夋嫨绛栫暐绫诲瀷'
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ瓥鐣ュ悕绉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createWaveRuleFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildWaveRuleDialogModel(props.waveRuleData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.waveRuleData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/manager/wave-rule/waveRulePage.helpers.js b/rsf-design/src/views/manager/wave-rule/waveRulePage.helpers.js
new file mode 100644
index 0000000..d96c6ac
--- /dev/null
+++ b/rsf-design/src/views/manager/wave-rule/waveRulePage.helpers.js
@@ -0,0 +1,114 @@
+const WAVE_RULE_DICT_TYPE_CODE = 'sys_wave_rule_code'
+
+export function getWaveRuleDictTypeCode() {
+ return WAVE_RULE_DICT_TYPE_CODE
+}
+
+export function createWaveRuleSearchState() {
+ return {
+ condition: '',
+ code: '',
+ type: '',
+ name: '',
+ status: ''
+ }
+}
+
+export function createWaveRuleFormState() {
+ return {
+ id: null,
+ code: '',
+ type: '',
+ name: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getWaveRulePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getWaveRuleStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '姝e父', type: 'success', bool: true }
+ : { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export function buildWaveRuleSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ code: String(params.code || '').trim(),
+ name: String(params.name || '').trim(),
+ ...(params.type !== '' && params.type !== null && params.type !== undefined
+ ? { type: Number(params.type) }
+ : {}),
+ ...(params.status !== '' && params.status !== null && params.status !== undefined
+ ? { status: Number(params.status) }
+ : {})
+ }
+}
+
+export function buildWaveRulePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWaveRuleSearchParams(params)
+ }
+}
+
+export function buildWaveRuleDialogModel(record = {}) {
+ return {
+ ...createWaveRuleFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ code: record.code || '',
+ type: record.type !== undefined && record.type !== null && record.type !== ''
+ ? Number(record.type)
+ : '',
+ name: record.name || '',
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildWaveRuleSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ ...(formData.code ? { code: String(formData.code).trim() } : {}),
+ type: Number(formData.type),
+ name: String(formData.name || '').trim(),
+ status: Number(formData.status ?? 1),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function buildWaveRuleTypeOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => ({
+ label: item.label || item.name || String(item.value ?? ''),
+ value: Number(item.value)
+ }))
+}
+
+export function normalizeWaveRuleListRow(record = {}) {
+ const statusMeta = getWaveRuleStatusMeta(record.status)
+ return {
+ ...record,
+ code: record.code || '',
+ name: record.name || '',
+ memo: record.memo || '',
+ typeText: record['type$'] || record.typeText || '-',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateByLabel: record['updateBy$'] || record.updateByLabel || '',
+ createByLabel: record['createBy$'] || record.createByLabel || '',
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
diff --git a/rsf-design/src/views/manager/wave-rule/waveRuleTable.columns.js b/rsf-design/src/views/manager/wave-rule/waveRuleTable.columns.js
new file mode 100644
index 0000000..f2bf1fd
--- /dev/null
+++ b/rsf-design/src/views/manager/wave-rule/waveRuleTable.columns.js
@@ -0,0 +1,73 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createWaveRuleTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'code',
+ label: '缂栧彿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'typeText',
+ label: '绫诲瀷',
+ minWidth: 140,
+ formatter: (row) => row.typeText || '-'
+ },
+ {
+ prop: 'name',
+ label: '鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateByLabel',
+ label: '鏇存柊浜�',
+ width: 120,
+ formatter: (row) => row.updateByLabel || '-'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/asn-order-item-log/asnOrderItemLogPage.helpers.js b/rsf-design/src/views/orders/asn-order-item-log/asnOrderItemLogPage.helpers.js
new file mode 100644
index 0000000..01469b7
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-item-log/asnOrderItemLogPage.helpers.js
@@ -0,0 +1,169 @@
+import { normalizeAsnOrderItemLogRow } from '../asn-order-log/asnOrderLogPage.helpers.js'
+
+export const ASN_ORDER_ITEM_LOG_REPORT_TITLE = '鏀惰揣鍘嗗彶鏄庣粏鎶ヨ〃'
+export const ASN_ORDER_ITEM_LOG_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+export function createAsnOrderItemLogSearchState(seed = {}) {
+ return {
+ condition: '',
+ logId: '',
+ orderId: '',
+ orderCode: '',
+ platItemId: '',
+ poDetlId: '',
+ poCode: '',
+ fieldsIndex: '',
+ matnrId: '',
+ matnrCode: '',
+ maktx: '',
+ anfme: '',
+ stockUnit: '',
+ purQty: '',
+ purUnit: '',
+ qty: '',
+ splrCode: '',
+ splrBatch: '',
+ splrName: '',
+ qrcode: '',
+ trackCode: '',
+ barcode: '',
+ packName: '',
+ ntyStatus: '',
+ memo: '',
+ status: '',
+ ...seed
+ }
+}
+
+export function getAsnOrderItemLogPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+function pushText(result, key, value) {
+ if (value === null || value === undefined) {
+ return
+ }
+ const text = String(value).trim()
+ if (text) {
+ result[key] = text
+ }
+}
+
+function pushNumber(result, key, value) {
+ if (value === '' || value === null || value === undefined) {
+ return
+ }
+ const numericValue = Number(value)
+ if (!Number.isNaN(numericValue)) {
+ result[key] = numericValue
+ }
+}
+
+export function buildAsnOrderItemLogSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'orderCode',
+ 'platItemId',
+ 'poCode',
+ 'fieldsIndex',
+ 'matnrCode',
+ 'maktx',
+ 'stockUnit',
+ 'purUnit',
+ 'splrCode',
+ 'splrBatch',
+ 'splrName',
+ 'qrcode',
+ 'trackCode',
+ 'barcode',
+ 'packName',
+ 'memo'
+ ].forEach((key) => pushText(result, key, params[key]))
+
+ ;[
+ 'logId',
+ 'orderId',
+ 'poDetlId',
+ 'matnrId',
+ 'anfme',
+ 'purQty',
+ 'qty',
+ 'ntyStatus',
+ 'status'
+ ].forEach((key) => pushNumber(result, key, params[key]))
+
+ return result
+}
+
+export function buildAsnOrderItemLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildAsnOrderItemLogSearchParams(params)
+ }
+}
+
+export function getAsnOrderItemLogReportColumns() {
+ return [
+ { key: 'asnCode', label: 'ASN鍗曞彿' },
+ { key: 'platItemId', label: '骞冲彴琛屽彿' },
+ { key: 'poDetlId', label: 'PO鍗曟槑缁咺D' },
+ { key: 'poCode', label: 'PO鍗曞彿' },
+ { key: 'fieldsIndex', label: '鍔ㄦ�佸瓧娈电储寮�' },
+ { key: 'matnrCode', label: '鐗╂枡缂栫爜' },
+ { key: 'maktx', label: '鐗╂枡鍚嶇О' },
+ { key: 'anfme', label: '閫佽揣鏁伴噺' },
+ { key: 'stockUnit', label: '搴撳瓨鍗曚綅' },
+ { key: 'purQty', label: '閲囪喘鏁伴噺' },
+ { key: 'purUnit', label: '閲囪喘鍗曚綅' },
+ { key: 'qty', label: '宸叉敹鏁伴噺' },
+ { key: 'splrCode', label: '渚涘簲鍟嗙紪鐮�' },
+ { key: 'splrBatch', label: '渚涘簲鍟嗘壒娆�' },
+ { key: 'splrName', label: '渚涘簲鍟嗗悕绉�' },
+ { key: 'qrcode', label: '浜岀淮鐮�' },
+ { key: 'trackCode', label: '璺熻釜鐮�' },
+ { key: 'barcode', label: '鏉″舰鐮�' },
+ { key: 'packName', label: '鍖呰鍚嶇О' },
+ { key: 'ntyStatusText', label: '涓婃姤鐘舵��' },
+ { key: 'statusText', label: '鐘舵��' },
+ { key: 'updateByText', label: '鏇存柊浜�' },
+ { key: 'updateTimeText', label: '鏇存柊鏃堕棿' },
+ { key: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildAsnOrderItemLogPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeAsnOrderItemLogRow(record))
+}
+
+export function buildAsnOrderItemLogReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = ASN_ORDER_ITEM_LOG_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: ASN_ORDER_ITEM_LOG_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...ASN_ORDER_ITEM_LOG_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/asn-order-item-log/index.vue b/rsf-design/src/views/orders/asn-order-item-log/index.vue
new file mode 100644
index 0000000..68187eb
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-item-log/index.vue
@@ -0,0 +1,256 @@
+<template>
+ <div class="asn-order-item-log-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useRoute } from 'vue-router'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import {
+ fetchAsnOrderItemLogPage,
+ fetchExportAsnOrderItemLogReport,
+ fetchGetAsnOrderItemLogMany
+ } from '@/api/asn-order-log'
+ import { createAsnOrderItemLogColumns } from '../asn-order-log/asnOrderLogTable.columns.js'
+ import { normalizeAsnOrderItemLogRow } from '../asn-order-log/asnOrderLogPage.helpers.js'
+ import {
+ ASN_ORDER_ITEM_LOG_REPORT_STYLE,
+ ASN_ORDER_ITEM_LOG_REPORT_TITLE,
+ buildAsnOrderItemLogPageQueryParams,
+ buildAsnOrderItemLogPrintRows,
+ buildAsnOrderItemLogReportMeta,
+ buildAsnOrderItemLogSearchParams,
+ createAsnOrderItemLogSearchState,
+ getAsnOrderItemLogPaginationKey,
+ getAsnOrderItemLogReportColumns
+ } from './asnOrderItemLogPage.helpers.js'
+
+ defineOptions({ name: 'AsnOrderItemLog' })
+
+ const route = useRoute()
+ const userStore = useUserStore()
+ const initialLogId = route.query.logId || route.query.id
+ const searchForm = ref(
+ createAsnOrderItemLogSearchState({
+ logId: initialLogId !== undefined ? Number(initialLogId) || '' : ''
+ })
+ )
+ const selectedRows = ref([])
+ const reportTitle = ASN_ORDER_ITEM_LOG_REPORT_TITLE
+ const reportColumns = getAsnOrderItemLogReportColumns()
+ const reportQueryParams = computed(() => buildAsnOrderItemLogSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏SN鍗曞彿/PO鍗曞彿/鐗╂枡缂栫爜'
+ }
+ },
+ {
+ label: '鏃ュ織ID',
+ key: 'logId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ棩蹇桰D'
+ }
+ },
+ {
+ label: 'ASN鍗曞彿',
+ key: 'orderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏SN鍗曞彿'
+ }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'poCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏O鍗曞彿'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '涓婃姤鐘舵��',
+ key: 'ntyStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鏈笂鎶�', value: 0 },
+ { label: '宸蹭笂鎶�', value: 1 },
+ { label: '閮ㄥ垎涓婃姤', value: 2 }
+ ]
+ }
+ }
+ ])
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchAsnOrderItemLogPage,
+ apiParams: buildAsnOrderItemLogPageQueryParams(searchForm.value),
+ paginationKey: getAsnOrderItemLogPaginationKey(),
+ columnsFactory: () => createAsnOrderItemLogColumns()
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeAsnOrderItemLogRow(item)) : []
+ }
+ })
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildAsnOrderItemLogPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ const resetSeed = initialLogId !== undefined ? { logId: Number(initialLogId) || '' } : {}
+ Object.assign(searchForm.value, createAsnOrderItemLogSearchState(resetSeed))
+ resetSearchParams(buildAsnOrderItemLogPageQueryParams(createAsnOrderItemLogSearchState(resetSeed)))
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetAsnOrderItemLogMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchAsnOrderItemLogPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'asn-order-item-log.xlsx',
+ requestExport: (payload) =>
+ fetchExportAsnOrderItemLogReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildAsnOrderItemLogPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...ASN_ORDER_ITEM_LOG_REPORT_STYLE
+ }
+ })
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildAsnOrderItemLogReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || ASN_ORDER_ITEM_LOG_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js b/rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js
new file mode 100644
index 0000000..d5c43d2
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-item/asnOrderItemPage.helpers.js
@@ -0,0 +1,245 @@
+export const ASN_ORDER_ITEM_REPORT_TITLE = '鏀惰揣鏄庣粏鎶ヨ〃'
+export const ASN_ORDER_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const STATUS_MAP = {
+ 1: { label: '姝e父', tagType: 'success' },
+ 0: { label: '鍐荤粨', tagType: 'info' }
+}
+
+const NTY_STATUS_MAP = {
+ 0: { label: '鏈笂鎶�', tagType: 'info' },
+ 1: { label: '宸蹭笂鎶�', tagType: 'success' }
+}
+
+function normalizeText(value, fallback = '-') {
+ if (value === null || value === undefined || value === '') {
+ return fallback
+ }
+ return String(value)
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+function pushText(result, key, value) {
+ const text = normalizeText(value, '')
+ if (text) {
+ result[key] = text
+ }
+}
+
+function pushNumber(result, key, value) {
+ if (value === '' || value === null || value === undefined) {
+ return
+ }
+ const numericValue = Number(value)
+ if (Number.isFinite(numericValue)) {
+ result[key] = numericValue
+ }
+}
+
+function pushRange(result, key, value) {
+ if (!Array.isArray(value) || value.length === 0) {
+ return
+ }
+ const normalizedRange = value.filter((item) => item !== null && item !== undefined && item !== '')
+ if (normalizedRange.length === 2) {
+ result[key] = normalizedRange
+ }
+}
+
+function getOrderTypeLabel(type) {
+ if (type === 'in') return '鍏ュ簱'
+ if (type === 'out') return '鍑哄簱'
+ return normalizeText(type, '-')
+}
+
+function getStatusConfig(status) {
+ const numericStatus = Number(status)
+ return STATUS_MAP[numericStatus] || { label: normalizeText(status, '-'), tagType: 'info' }
+}
+
+function getNtyStatusConfig(status) {
+ const numericStatus = Number(status)
+ return NTY_STATUS_MAP[numericStatus] || { label: normalizeText(status, '-'), tagType: 'info' }
+}
+
+export function createAsnOrderItemSearchState() {
+ return {
+ condition: '',
+ orderCode: '',
+ poCode: '',
+ platWorkCode: '',
+ platItemId: '',
+ matnrCode: '',
+ maktx: '',
+ spec: '',
+ model: '',
+ batch: '',
+ stockUnit: '',
+ purUnit: '',
+ splrCode: '',
+ splrName: '',
+ splrBatch: '',
+ qrcode: '',
+ trackCode: '',
+ packName: '',
+ projectCode: '',
+ targetWarehouseId: '',
+ status: '',
+ ntyStatus: '',
+ createTimeRange: [],
+ updateTimeRange: []
+ }
+}
+
+export function buildAsnOrderItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'orderCode',
+ 'poCode',
+ 'platWorkCode',
+ 'platItemId',
+ 'matnrCode',
+ 'maktx',
+ 'spec',
+ 'model',
+ 'batch',
+ 'stockUnit',
+ 'purUnit',
+ 'splrCode',
+ 'splrName',
+ 'splrBatch',
+ 'qrcode',
+ 'trackCode',
+ 'packName',
+ 'projectCode',
+ 'targetWarehouseId'
+ ].forEach((key) => pushText(result, key, params[key]))
+
+ ;['status', 'ntyStatus'].forEach((key) => pushNumber(result, key, params[key]))
+ ;['createTimeRange', 'updateTimeRange'].forEach((key) => pushRange(result, key, params[key]))
+
+ return result
+}
+
+export function buildAsnOrderItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.orderBy ? { orderBy: normalizeText(params.orderBy) } : {}),
+ ...buildAsnOrderItemSearchParams(params)
+ }
+}
+
+export function normalizeAsnOrderItemRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? null,
+ poCode: normalizeText(record.poCode, '-'),
+ typeLabel: getOrderTypeLabel(record.type),
+ wkTypeLabel: normalizeText(record['wkType$'] || record.wkType, '-'),
+ purchaseOrgName: normalizeText(record.purchaseOrgName, '-'),
+ purchaseUserName: normalizeText(record.purchaseUserName, '-'),
+ supplierId: normalizeText(record.supplierId, '-'),
+ supplierName: normalizeText(record.supplierName, '-'),
+ platWorkCode: normalizeText(record.platWorkCode, '-'),
+ platItemId: normalizeText(record.platItemId, '-'),
+ matnrCode: normalizeText(record.matnrCode, '-'),
+ maktx: normalizeText(record.maktx, '-'),
+ batch: normalizeText(record.batch, '-'),
+ stockUnit: normalizeText(record.stockUnit, '-'),
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ targetWarehouseId: normalizeText(record.targetWarehouseId, '-'),
+ businessTimeText: normalizeText(record.businessTime, '-'),
+ updateTimeText: normalizeText(record.updateTime, '-'),
+ memo: normalizeText(record.memo, '-')
+ }
+}
+
+export function normalizeAsnOrderItemDetail(record = {}) {
+ const statusConfig = getStatusConfig(record.status)
+ const ntyStatusConfig = getNtyStatusConfig(record.ntyStatus)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ orderCode: normalizeText(record.orderCode, '-'),
+ poCode: normalizeText(record.poCode, '-'),
+ typeLabel: getOrderTypeLabel(record.type),
+ poDetlId: record.poDetlId ?? '-',
+ platWorkCode: normalizeText(record.platWorkCode, '-'),
+ platItemId: normalizeText(record.platItemId, '-'),
+ projectCode: normalizeText(record.projectCode, '-'),
+ matnrCode: normalizeText(record.matnrCode, '-'),
+ maktx: normalizeText(record.maktx, '-'),
+ spec: normalizeText(record.spec, '-'),
+ model: normalizeText(record.model, '-'),
+ batch: normalizeText(record.batch, '-'),
+ stockUnit: normalizeText(record.stockUnit, '-'),
+ purQty: normalizeNumber(record.purQty),
+ purUnit: normalizeText(record.purUnit, '-'),
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ workQty: normalizeNumber(record.workQty),
+ splrCode: normalizeText(record.splrCode, '-'),
+ splrName: normalizeText(record.splrName, '-'),
+ splrBatch: normalizeText(record.splrBatch, '-'),
+ qrcode: normalizeText(record.qrcode, '-'),
+ barcode: normalizeText(record.barcode, '-'),
+ packName: normalizeText(record.packName, '-'),
+ prodTimeText: normalizeText(record.prodTime, '-'),
+ targetWarehouseId: normalizeText(record.targetWarehouseId, '-'),
+ sourceWarehouseId: normalizeText(record.sourceWarehouseId, '-'),
+ ntyStatusText: ntyStatusConfig.label,
+ ntyStatusTagType: ntyStatusConfig.tagType,
+ statusText: statusConfig.label,
+ statusTagType: statusConfig.tagType,
+ isptResultText: normalizeText(record['isptResult$'] || record.isptResult, '-'),
+ updateByText: normalizeText(record['updateBy$'] || record.updateBy, '-'),
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTime, '-'),
+ createByText: normalizeText(record['createBy$'] || record.createBy, '-'),
+ createTimeText: normalizeText(record['createTime$'] || record.createTime, '-'),
+ memo: normalizeText(record.memo, '-'),
+ extendFields: record.extendFields || {}
+ }
+}
+
+export function buildAsnOrderItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeAsnOrderItemRow(record))
+}
+
+export function buildAsnOrderItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = ASN_ORDER_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: ASN_ORDER_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...ASN_ORDER_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/asn-order-item/asnOrderItemTable.columns.js b/rsf-design/src/views/orders/asn-order-item/asnOrderItemTable.columns.js
new file mode 100644
index 0000000..3306ced
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-item/asnOrderItemTable.columns.js
@@ -0,0 +1,66 @@
+import { h } from 'vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+function createTextColumn(prop, label, minWidth, { align, formatter } = {}) {
+ return {
+ prop,
+ label,
+ minWidth,
+ align,
+ showOverflowTooltip: true,
+ formatter: formatter || ((row) => row?.[prop] || '-')
+ }
+}
+
+export function createAsnOrderItemTableColumns({ handleView } = {}) {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ createTextColumn('poCode', 'PO鍗曞彿', 160),
+ createTextColumn('wkTypeLabel', '涓氬姟绫诲瀷', 120),
+ createTextColumn('typeLabel', '鍗曟嵁绫诲瀷', 110),
+ createTextColumn('purchaseOrgName', '閲囪喘缁勭粐', 150),
+ createTextColumn('purchaseUserName', '閲囪喘鍛�', 120),
+ createTextColumn('supplierName', '渚涘簲鍟�', 160),
+ createTextColumn('platWorkCode', '璁″垝璺熻釜鍙�', 150),
+ createTextColumn('platItemId', '琛屽彿', 110),
+ createTextColumn('matnrCode', '鐗╂枡缂栫爜', 160),
+ createTextColumn('maktx', '鐗╂枡鍚嶇О', 220),
+ createTextColumn('batch', '渚涘簲鍟嗘壒娆�', 140),
+ createTextColumn('stockUnit', '搴撳瓨鍗曚綅', 110),
+ {
+ prop: 'anfme',
+ label: '閫佽揣鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.anfme ?? 0
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.qty ?? 0
+ },
+ createTextColumn('targetWarehouseId', '寤鸿鐩爣浠�', 140),
+ createTextColumn('businessTimeText', '涓氬姟鏃堕棿', 180),
+ createTextColumn('updateTimeText', '鏇存柊鏃堕棿', 180),
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ align: 'right',
+ formatter: (row) =>
+ h(
+ 'div',
+ { class: 'flex justify-end' },
+ [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ ]
+ )
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/asn-order-item/index.vue b/rsf-design/src/views/orders/asn-order-item/index.vue
new file mode 100644
index 0000000..f1f4cce
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-item/index.vue
@@ -0,0 +1,386 @@
+<template>
+ <div class="asn-order-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <AsnOrderItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchAsnOrderItemFullPage,
+ fetchExportAsnOrderItemReport,
+ fetchGetAsnOrderItemDetail
+ } from '@/api/asn-order-item'
+ import AsnOrderItemDetailDrawer from './modules/asn-order-item-detail-drawer.vue'
+ import { createAsnOrderItemTableColumns } from './asnOrderItemTable.columns'
+ import {
+ ASN_ORDER_ITEM_REPORT_STYLE,
+ ASN_ORDER_ITEM_REPORT_TITLE,
+ buildAsnOrderItemPageQueryParams,
+ buildAsnOrderItemPrintRows,
+ buildAsnOrderItemReportMeta,
+ buildAsnOrderItemSearchParams,
+ createAsnOrderItemSearchState,
+ normalizeAsnOrderItemDetail,
+ normalizeAsnOrderItemRow
+ } from './asnOrderItemPage.helpers'
+
+ defineOptions({ name: 'AsnOrderItem' })
+
+ const DEFAULT_PAGE_SIZE = 20
+
+ const userStore = useUserStore()
+ const reportTitle = ASN_ORDER_ITEM_REPORT_TITLE
+ const searchForm = ref(createAsnOrderItemSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const activeItemId = ref(null)
+
+ const reportQueryParams = computed(() => ({
+ ...buildAsnOrderItemSearchParams(searchForm.value),
+ orderBy: 'id desc'
+ }))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� PO 鍗曞彿/鐗╂枡缂栫爜/鐗╂枡鍚嶇О/渚涘簲鍟�'
+ }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'poCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� PO 鍗曞彿'
+ }
+ },
+ {
+ label: 'ASN鍗曞彿',
+ key: 'orderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� ASN 鍗曞彿'
+ }
+ },
+ {
+ label: '璁″垝璺熻釜鍙�',
+ key: 'platWorkCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鍒掕窡韪彿'
+ }
+ },
+ {
+ label: '琛屽彿',
+ key: 'platItemId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鍙�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '搴撳瓨鍗曚綅',
+ key: 'stockUnit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樺崟浣�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '涓婃姤鐘舵��',
+ key: 'ntyStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鏈笂鎶�', value: 0 },
+ { label: '宸蹭笂鎶�', value: 1 }
+ ]
+ }
+ },
+ {
+ label: '鍒涘缓鏃堕棿',
+ key: 'createTimeRange',
+ type: 'datetimerange',
+ props: {
+ clearable: true,
+ startPlaceholder: '寮�濮嬫椂闂�',
+ endPlaceholder: '缁撴潫鏃堕棿',
+ rangeSeparator: '鑷�'
+ }
+ },
+ {
+ label: '鏇存柊鏃堕棿',
+ key: 'updateTimeRange',
+ type: 'datetimerange',
+ props: {
+ clearable: true,
+ startPlaceholder: '寮�濮嬫椂闂�',
+ endPlaceholder: '缁撴潫鏃堕棿',
+ rangeSeparator: '鑷�'
+ }
+ }
+ ])
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchAsnOrderItemFullPage,
+ apiParams: buildAsnOrderItemPageQueryParams({
+ ...searchForm.value,
+ pageSize: DEFAULT_PAGE_SIZE,
+ orderBy: 'id desc'
+ }),
+ columnsFactory: () =>
+ createAsnOrderItemTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeAsnOrderItemRow(item)) : []
+ }
+ })
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(
+ buildAsnOrderItemPageQueryParams({
+ ...searchForm.value,
+ orderBy: 'id desc'
+ })
+ )
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createAsnOrderItemSearchState()
+ resetSearchParams()
+ }
+
+ async function openDetail(row) {
+ activeItemId.value = row.id
+ detailData.value = normalizeAsnOrderItemDetail(row)
+ detailDrawerVisible.value = true
+ await loadDetailResource()
+ }
+
+ async function loadDetailResource() {
+ if (!activeItemId.value) {
+ return
+ }
+
+ detailLoading.value = true
+ try {
+ const detailResponse = await guardRequestWithMessage(
+ fetchGetAsnOrderItemDetail(activeItemId.value),
+ {},
+ {
+ timeoutMessage: '鏀惰揣鏄庣粏璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+
+ detailData.value = normalizeAsnOrderItemDetail({
+ ...detailData.value,
+ ...detailResponse
+ })
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鏀惰揣鏄庣粏璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function loadAllReportRows() {
+ const reportPageSize = Number(pagination.total) > 0 ? Number(pagination.total) : DEFAULT_PAGE_SIZE
+ const response = await guardRequestWithMessage(
+ fetchAsnOrderItemFullPage(
+ buildAsnOrderItemPageQueryParams({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: reportPageSize,
+ orderBy: 'id desc'
+ })
+ ),
+ { records: [], total: 0, current: 1, size: reportPageSize },
+ {
+ timeoutMessage: '鏀惰揣鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'asn-order-item.xlsx',
+ requestExport: async () => {
+ const records = await loadAllReportRows()
+ const ids = records
+ .map((item) => item?.id)
+ .filter((id) => id !== undefined && id !== null)
+
+ if (ids.length === 0) {
+ throw new Error('鏆傛棤鍙鍑虹殑鏁版嵁')
+ }
+
+ return fetchExportAsnOrderItemReport(
+ { ids },
+ {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }
+ )
+ },
+ resolvePrintRecords: async () => loadAllReportRows(),
+ buildPreviewRows: (records) => buildAsnOrderItemPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...ASN_ORDER_ITEM_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildAsnOrderItemReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation ||
+ ASN_ORDER_ITEM_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/asn-order-item/modules/asn-order-item-detail-drawer.vue b/rsf-design/src/views/orders/asn-order-item/modules/asn-order-item-detail-drawer.vue
new file mode 100644
index 0000000..edd2178
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-item/modules/asn-order-item-detail-drawer.vue
@@ -0,0 +1,107 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鏀惰揣鏄庣粏璇︽儏"
+ size="1180px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="PO鍗曞彿">{{ detail.poCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ASN鍗曞彿">{{ detail.orderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘缁勭粐">{{ detail.purchaseOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘鍛�">{{ detail.purchaseUserName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟咺D">{{ detail.supplierId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗗悕绉�">{{ detail.supplierName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟鏃堕棿">{{ detail.businessTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寤鸿鐩爣浠�">{{ detail.targetWarehouseId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusTagType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="涓婃姤鐘舵��">
+ <ElTag :type="detail.ntyStatusTagType || 'info'" effect="light">
+ {{ detail.ntyStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="鏄庣粏淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="璁″垝琛屽彿">{{ detail.platItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁″垝璺熻釜鍙�">{{ detail.platWorkCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勬牸">{{ detail.spec || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍨嬪彿">{{ detail.model || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉″舰鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浜岀淮鐮�">{{ detail.qrcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍖呰鍚嶇О">{{ detail.packName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鍗曚綅">{{ detail.stockUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘鍗曚綅">{{ detail.purUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閫佽揣鏁伴噺">{{ detail.anfme ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸叉敹鏁伴噺">{{ detail.qty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘鏁伴噺">{{ detail.purQty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐢熶骇鏃ユ湡">{{ detail.prodTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐ㄦ缁撴灉">{{ detail.isptResultText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉ユ簮浠撳簱">{{ detail.sourceWarehouseId || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions
+ v-if="Object.keys(detail.extendFields || {}).length > 0"
+ title="鎵╁睍瀛楁"
+ :column="2"
+ border
+ >
+ <ElDescriptionsItem
+ v-for="[key, value] in Object.entries(detail.extendFields || {})"
+ :key="key"
+ :label="key"
+ >
+ {{ value || '--' }}
+ </ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ defineOptions({ name: 'AsnOrderItemDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/orders/asn-order-log/asnOrderLogPage.helpers.js b/rsf-design/src/views/orders/asn-order-log/asnOrderLogPage.helpers.js
new file mode 100644
index 0000000..49a1d8c
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-log/asnOrderLogPage.helpers.js
@@ -0,0 +1,306 @@
+export const ASN_ORDER_LOG_REPORT_TITLE = '鍘嗗彶閫氱煡鍗曟姤琛�'
+export const ASN_ORDER_LOG_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const RLE_STATUS_META = {
+ 0: { text: '姝e父', type: 'info' },
+ 1: { text: '宸查噴鏀�', type: 'success' }
+}
+
+const NTY_STATUS_META = {
+ 0: { text: '鏈笂鎶�', type: 'info' },
+ 1: { text: '宸蹭笂鎶�', type: 'success' },
+ 2: { text: '閮ㄥ垎涓婃姤', type: 'warning' }
+}
+
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'danger' }
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeDateText(value) {
+ return normalizeText(value)
+}
+
+function getStatusMeta(value, map, fallbackText = '-') {
+ const numericValue = Number(value)
+ const meta = map[numericValue]
+ if (meta) {
+ return meta
+ }
+ const text = normalizeText(value)
+ if (!text) {
+ return { text: fallbackText, type: 'info' }
+ }
+ return { text, type: 'info' }
+}
+
+function normalizeTagText(value, map) {
+ const text = normalizeText(value)
+ if (text) {
+ return text
+ }
+ const numericValue = Number(value)
+ const meta = map[numericValue]
+ return meta?.text || '-'
+}
+
+export function createAsnOrderLogSearchState() {
+ return {
+ condition: '',
+ code: '',
+ poCode: '',
+ poId: '',
+ type: '',
+ wkType: '',
+ logisNo: '',
+ arrTime: '',
+ rleStatus: '',
+ ntyStatus: '',
+ exceStatus: '',
+ status: ''
+ }
+}
+
+export function buildAsnOrderLogSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ poCode: normalizeText(params.poCode),
+ poId:
+ params.poId !== undefined && params.poId !== null && params.poId !== ''
+ ? normalizeNumber(params.poId)
+ : void 0,
+ type: normalizeText(params.type),
+ wkType: normalizeText(params.wkType),
+ logisNo: normalizeText(params.logisNo),
+ arrTime: normalizeText(params.arrTime),
+ rleStatus:
+ params.rleStatus !== undefined && params.rleStatus !== null && params.rleStatus !== ''
+ ? normalizeNumber(params.rleStatus)
+ : void 0,
+ ntyStatus:
+ params.ntyStatus !== undefined && params.ntyStatus !== null && params.ntyStatus !== ''
+ ? normalizeNumber(params.ntyStatus)
+ : void 0,
+ exceStatus:
+ params.exceStatus !== undefined && params.exceStatus !== null && params.exceStatus !== ''
+ ? normalizeNumber(params.exceStatus)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(
+ ([, value]) => value !== '' && value !== void 0 && value !== null
+ )
+ )
+}
+
+export function buildAsnOrderLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildAsnOrderLogSearchParams(params)
+ }
+}
+
+export function buildAsnOrderLogDetailQueryParams(params = {}) {
+ return {
+ logId: params.logId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function normalizeAsnOrderLogRow(record = {}) {
+ const rleStatusMeta = getStatusMeta(record.rleStatus, RLE_STATUS_META)
+ const ntyStatusMeta = getStatusMeta(record.ntyStatus, NTY_STATUS_META)
+ const statusMeta = getStatusMeta(record.status, STATUS_META)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ asnId: record.asnId ?? '--',
+ code: normalizeText(record.code) || '--',
+ poCode: normalizeText(record.poCode) || '--',
+ poId: record.poId ?? '--',
+ type: normalizeText(record.type) || '',
+ typeText: normalizeTagText(record['type$'] || record.typeText || record.type, {}),
+ wkType: normalizeText(record.wkType) || '',
+ wkTypeText: normalizeTagText(record['wkType$'] || record.wkTypeText || record.wkType, {}),
+ anfme: record.anfme ?? '--',
+ qty: record.qty ?? '--',
+ logisNo: normalizeText(record.logisNo) || '--',
+ arrTime: record.arrTime ?? null,
+ arrTimeText: normalizeDateText(record['arrTime$'] || record.arrTime) || '--',
+ rleStatus: record.rleStatus ?? '--',
+ rleStatusText: normalizeTagText(record['rleStatus$'] || rleStatusMeta.text, RLE_STATUS_META),
+ rleStatusTagType: rleStatusMeta.type,
+ ntyStatus: record.ntyStatus ?? '--',
+ ntyStatusText: normalizeTagText(record['ntyStatus$'] || ntyStatusMeta.text, NTY_STATUS_META),
+ ntyStatusTagType: ntyStatusMeta.type,
+ exceStatus: record.exceStatus ?? '--',
+ exceStatusText: normalizeTagText(record['exceStatus$'] || record.exceStatusText || record.exceStatus, {}),
+ status: record.status ?? '--',
+ statusText: normalizeTagText(record['status$'] || statusMeta.text, STATUS_META),
+ statusType: statusMeta.type,
+ createByText: normalizeText(record['createBy$'] || record.createByText || '') || '--',
+ createTimeText: normalizeDateText(record['createTime$'] || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText || '') || '--',
+ updateTimeText: normalizeDateText(record['updateTime$'] || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function normalizeAsnOrderItemLogRow(record = {}) {
+ const ntyStatusMeta = getStatusMeta(record.ntyStatus, NTY_STATUS_META)
+ const statusMeta = getStatusMeta(record.status, STATUS_META)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ logId: record.logId ?? '--',
+ asnItemId: record.asnItemId ?? '--',
+ asnId: record.asnId ?? '--',
+ asnCode: normalizeText(record.asnCode) || '--',
+ platItemId: normalizeText(record.platItemId) || '--',
+ poDetlId: record.poDetlId ?? '--',
+ poCode: normalizeText(record.poCode) || '--',
+ fieldsIndex: normalizeText(record.fieldsIndex) || '--',
+ matnrId: record.matnrId ?? '--',
+ matnrCode: normalizeText(record.matnrCode) || '--',
+ maktx: normalizeText(record.maktx) || '--',
+ anfme: record.anfme ?? '--',
+ stockUnit: normalizeText(record.stockUnit) || '--',
+ purQty: record.purQty ?? '--',
+ purUnit: normalizeText(record.purUnit) || '--',
+ qty: record.qty ?? '--',
+ splrCode: normalizeText(record.splrCode) || '--',
+ splrBatch: normalizeText(record.splrBatch) || '--',
+ splrName: normalizeText(record.splrName) || '--',
+ qrcode: normalizeText(record.qrcode) || '--',
+ trackCode: normalizeText(record.trackCode) || '--',
+ barcode: normalizeText(record.barcode) || '--',
+ packName: normalizeText(record.packName) || '--',
+ ntyStatus: record.ntyStatus ?? '--',
+ ntyStatusText: normalizeTagText(record['ntyStatus$'] || ntyStatusMeta.text, NTY_STATUS_META),
+ ntyStatusTagType: ntyStatusMeta.type,
+ status: record.status ?? '--',
+ statusText: normalizeTagText(record['status$'] || statusMeta.text, STATUS_META),
+ statusType: statusMeta.type,
+ createByText: normalizeText(record['createBy$'] || record.createByText || '') || '--',
+ createTimeText: normalizeDateText(record['createTime$'] || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText || '') || '--',
+ updateTimeText: normalizeDateText(record['updateTime$'] || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function buildAsnOrderLogPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeAsnOrderLogRow(record))
+}
+
+export function buildAsnOrderLogReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = ASN_ORDER_LOG_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: ASN_ORDER_LOG_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...ASN_ORDER_LOG_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function getAsnOrderLogRleStatusOptions() {
+ return [
+ { label: '姝e父', value: 0 },
+ { label: '宸查噴鏀�', value: 1 }
+ ]
+}
+
+export function getAsnOrderLogNtyStatusOptions() {
+ return [
+ { label: '鏈笂鎶�', value: 0 },
+ { label: '宸蹭笂鎶�', value: 1 },
+ { label: '閮ㄥ垎涓婃姤', value: 2 }
+ ]
+}
+
+export function getAsnOrderLogStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getAsnOrderLogTypeOptions() {
+ return []
+}
+
+export function getAsnOrderLogWkTypeOptions() {
+ return []
+}
+
+export function getAsnOrderLogExceStatusOptions() {
+ return []
+}
+
+export function resolveDictOptions(records = [], options = {}) {
+ const { group } = options
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .filter((item) => {
+ if (!item || typeof item !== 'object') {
+ return false
+ }
+ if (group === undefined) {
+ return true
+ }
+ return normalizeText(item.group) === normalizeText(group)
+ })
+ .map((item) => {
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === undefined || value === null || value === '') {
+ return null
+ }
+ return {
+ value: normalizeText(value),
+ label: normalizeText(item.label || item.name || item.dictLabel || value)
+ }
+ })
+ .filter(Boolean)
+}
diff --git a/rsf-design/src/views/orders/asn-order-log/asnOrderLogTable.columns.js b/rsf-design/src/views/orders/asn-order-log/asnOrderLogTable.columns.js
new file mode 100644
index 0000000..417d2ae
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-log/asnOrderLogTable.columns.js
@@ -0,0 +1,313 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+function buildTagRenderer(textKey, typeKey) {
+ return (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.[typeKey] || 'info',
+ effect: 'light'
+ },
+ () => row?.[textKey] || '--'
+ )
+}
+
+export function createAsnOrderLogTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: 'ASN鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'poCode',
+ label: 'PO鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.poCode || '--'
+ },
+ {
+ prop: 'poId',
+ label: 'PO鍗旾D',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.poId ?? '--'
+ },
+ {
+ prop: 'typeText',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeText || '--'
+ },
+ {
+ prop: 'wkTypeText',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.wkTypeText || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '閫佽揣鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'logisNo',
+ label: '鐗╂祦鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.logisNo || '--'
+ },
+ {
+ prop: 'arrTimeText',
+ label: '棰勮鍒拌揪鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.arrTimeText || '--'
+ },
+ {
+ prop: 'rleStatusText',
+ label: '閲婃斁鐘舵��',
+ width: 110,
+ align: 'center',
+ formatter: buildTagRenderer('rleStatusText', 'rleStatusTagType')
+ },
+ {
+ prop: 'ntyStatusText',
+ label: '涓婃姤鐘舵��',
+ width: 110,
+ align: 'center',
+ formatter: buildTagRenderer('ntyStatusText', 'ntyStatusTagType')
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鎵ц鐘舵��',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.exceStatusText || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: buildTagRenderer('statusText', 'statusType')
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
+
+export function createAsnOrderItemLogColumns() {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'asnCode',
+ label: 'ASN鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.asnCode || '--'
+ },
+ {
+ prop: 'platItemId',
+ label: '骞冲彴琛屽彿',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platItemId || '--'
+ },
+ {
+ prop: 'poDetlId',
+ label: 'PO鍗曟槑缁咺D',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.poDetlId ?? '--'
+ },
+ {
+ prop: 'poCode',
+ label: 'PO鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.poCode || '--'
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '鍔ㄦ�佸瓧娈电储寮�',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.fieldsIndex || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '閫佽揣鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'stockUnit',
+ label: '搴撳瓨鍗曚綅',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.stockUnit || '--'
+ },
+ {
+ prop: 'purQty',
+ label: '閲囪喘鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.purQty ?? '--'
+ },
+ {
+ prop: 'purUnit',
+ label: '閲囪喘鍗曚綅',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.purUnit || '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'splrCode',
+ label: '渚涘簲鍟嗙紪鐮�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrCode || '--'
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrBatch || '--'
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟嗗悕绉�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrName || '--'
+ },
+ {
+ prop: 'qrcode',
+ label: '浜岀淮鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.qrcode || '--'
+ },
+ {
+ prop: 'trackCode',
+ label: '璺熻釜鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.trackCode || '--'
+ },
+ {
+ prop: 'barcode',
+ label: '鏉″舰鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.barcode || '--'
+ },
+ {
+ prop: 'packName',
+ label: '鍖呰鍚嶇О',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.packName || '--'
+ },
+ {
+ prop: 'ntyStatusText',
+ label: '涓婃姤鐘舵��',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.ntyStatusText || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => row.statusText || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/asn-order-log/index.vue b/rsf-design/src/views/orders/asn-order-log/index.vue
new file mode 100644
index 0000000..968c48a
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-log/index.vue
@@ -0,0 +1,468 @@
+<template>
+ <div class="asn-order-log-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <AsnOrderLogDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :items-loading="detailItemsLoading"
+ :detail="detailData"
+ :item-rows="detailItemRows"
+ :item-columns="detailItemColumns"
+ :pagination="detailPagination"
+ @refresh="loadDetailResources"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import {
+ buildAsnOrderLogDetailQueryParams,
+ buildAsnOrderLogPageQueryParams,
+ buildAsnOrderLogPrintRows,
+ buildAsnOrderLogReportMeta,
+ buildAsnOrderLogSearchParams,
+ createAsnOrderLogSearchState,
+ getAsnOrderLogExceStatusOptions,
+ getAsnOrderLogNtyStatusOptions,
+ getAsnOrderLogRleStatusOptions,
+ getAsnOrderLogStatusOptions,
+ normalizeAsnOrderItemLogRow,
+ normalizeAsnOrderLogRow,
+ resolveDictOptions,
+ ASN_ORDER_LOG_REPORT_STYLE,
+ ASN_ORDER_LOG_REPORT_TITLE
+ } from './asnOrderLogPage.helpers'
+ import {
+ fetchAsnOrderItemLogPage,
+ fetchAsnOrderLogPage,
+ fetchExportAsnOrderLogReport,
+ fetchGetAsnOrderLogDetail,
+ fetchGetAsnOrderLogMany
+ } from '@/api/asn-order-log'
+ import AsnOrderLogDetailDrawer from './modules/asn-order-log-detail-drawer.vue'
+ import {
+ createAsnOrderItemLogColumns,
+ createAsnOrderLogTableColumns
+ } from './asnOrderLogTable.columns'
+
+ defineOptions({ name: 'AsnOrderLog' })
+
+ const userStore = useUserStore()
+ const reportTitle = ASN_ORDER_LOG_REPORT_TITLE
+ const searchForm = ref(createAsnOrderLogSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailItemsLoading = ref(false)
+ const detailData = ref({})
+ const detailItemRows = ref([])
+ const activeLogId = ref(null)
+ const typeOptions = ref([])
+ const wkTypeOptions = ref([])
+ const exceStatusOptions = ref([])
+ const detailItemColumns = createAsnOrderItemLogColumns()
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildAsnOrderLogSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏SN鍗曞彿/PO鍗曞彿/鐗╂祦鍗曞彿'
+ }
+ },
+ {
+ label: 'ASN鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏SN鍗曞彿'
+ }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'poCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏O鍗曞彿'
+ }
+ },
+ {
+ label: 'PO鍗旾D',
+ key: 'poId',
+ type: 'number',
+ props: {
+ min: 0,
+ controls: false,
+ placeholder: '璇疯緭鍏O鍗旾D'
+ }
+ },
+ {
+ label: '鍗曟嵁绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: typeOptions.value
+ }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: wkTypeOptions.value
+ }
+ },
+ {
+ label: '鐗╂祦鍗曞彿',
+ key: 'logisNo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿娴佸崟鍙�'
+ }
+ },
+ {
+ label: '棰勮鍒拌揪鏃堕棿',
+ key: 'arrTime',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨鏃ユ湡'
+ }
+ },
+ {
+ label: '閲婃斁鐘舵��',
+ key: 'rleStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getAsnOrderLogRleStatusOptions()
+ }
+ },
+ {
+ label: '涓婃姤鐘舵��',
+ key: 'ntyStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getAsnOrderLogNtyStatusOptions()
+ }
+ },
+ {
+ label: '鎵ц鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: exceStatusOptions.value
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getAsnOrderLogStatusOptions()
+ }
+ }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ function openDetail(row) {
+ activeLogId.value = row.id
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ loadDetailResources()
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchAsnOrderLogPage,
+ apiParams: buildAsnOrderLogPageQueryParams(searchForm.value),
+ columnsFactory: () => createAsnOrderLogTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeAsnOrderLogRow(item)) : []
+ }
+ })
+
+ async function loadDetailResources() {
+ if (!activeLogId.value) {
+ return
+ }
+
+ detailLoading.value = true
+ detailItemsLoading.value = true
+ try {
+ const [detailResponse, itemResponse] = await Promise.all([
+ guardRequestWithMessage(
+ fetchGetAsnOrderLogDetail(activeLogId.value),
+ {},
+ {
+ timeoutMessage: '鍘嗗彶閫氱煡鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ ),
+ guardRequestWithMessage(
+ fetchAsnOrderItemLogPage(
+ buildAsnOrderLogDetailQueryParams({
+ logId: activeLogId.value,
+ current: detailPagination.current,
+ pageSize: detailPagination.size
+ })
+ ),
+ { records: [], total: 0, current: detailPagination.current, size: detailPagination.size },
+ {
+ timeoutMessage: '鍘嗗彶閫氱煡鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
+ ])
+
+ detailData.value = normalizeAsnOrderLogRow(detailResponse || {})
+ const itemResult = defaultResponseAdapter(itemResponse)
+ detailItemRows.value = Array.isArray(itemResult.records)
+ ? itemResult.records.map((item) => normalizeAsnOrderItemLogRow(item))
+ : []
+ updatePaginationState(
+ detailPagination,
+ itemResult,
+ detailPagination.current,
+ detailPagination.size
+ )
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ detailItemRows.value = []
+ ElMessage.error(error?.message || '鑾峰彇鍘嗗彶閫氱煡鍗曡鎯呭け璐�')
+ } finally {
+ detailLoading.value = false
+ detailItemsLoading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildAsnOrderLogSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createAsnOrderLogSearchState()
+ resetSearchParams()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ detailPagination.current = 1
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ function buildPreviewDialogMeta(rows) {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...ASN_ORDER_LOG_REPORT_STYLE
+ }
+ }
+ }
+
+ async function resolvePrintRecords(payload) {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetAsnOrderLogMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchAsnOrderLogPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'asn-order-log.xlsx',
+ requestExport: (payload) =>
+ fetchExportAsnOrderLogReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildAsnOrderLogPrintRows(records),
+ buildPreviewMeta: buildPreviewDialogMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildAsnOrderLogReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || ASN_ORDER_LOG_REPORT_STYLE.orientation
+ })
+ )
+
+ async function loadTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_order_type',
+ status: 1
+ }),
+ { records: [] },
+ {
+ timeoutMessage: '鍗曟嵁绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ typeOptions.value = resolveDictOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadWkTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_business_type',
+ group: 1,
+ status: 1
+ }),
+ { records: [] },
+ {
+ timeoutMessage: '涓氬姟绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ wkTypeOptions.value = resolveDictOptions(defaultResponseAdapter(response).records, {
+ group: 1
+ })
+ }
+
+ async function loadExceStatusOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_asn_exce_status',
+ status: 1
+ }),
+ { records: [] },
+ {
+ timeoutMessage: '鎵ц鐘舵�侀�夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ exceStatusOptions.value = resolveDictOptions(defaultResponseAdapter(response).records)
+ }
+
+ onMounted(async () => {
+ await Promise.allSettled([loadTypeOptions(), loadWkTypeOptions(), loadExceStatusOptions()])
+ })
+</script>
diff --git a/rsf-design/src/views/orders/asn-order-log/modules/asn-order-log-detail-drawer.vue b/rsf-design/src/views/orders/asn-order-log/modules/asn-order-log-detail-drawer.vue
new file mode 100644
index 0000000..529bd8b
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order-log/modules/asn-order-log-detail-drawer.vue
@@ -0,0 +1,78 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鍘嗗彶閫氱煡鍗曡鎯�"
+ size="88%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="asn-order-log-detail-scroll">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="ASN鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ASN涓诲崟ID">{{ detail.asnId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="PO鍗曞彿">{{ detail.poCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="PO鍗旾D">{{ detail.poId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detail.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閫佽揣鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸叉敹鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂祦鍗曞彿">{{ detail.logisNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="棰勮鍒拌揪鏃堕棿">{{ detail.arrTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲婃斁鐘舵��">{{ detail.rleStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓婃姤鐘舵��">{{ detail.ntyStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鐘舵��">{{ detail.exceStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="flex items-center justify-between">
+ <div class="text-sm text-[var(--art-gray-600)]">鍘嗗彶鏄庣粏</div>
+ <ElButton :loading="loading || itemsLoading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+
+ <ElCard shadow="never" class="border border-[var(--art-border-color)]">
+ <ArtTable
+ :loading="itemsLoading"
+ :data="itemRows"
+ :columns="itemColumns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </ElCard>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'AsnOrderLogDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ itemsLoading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ itemRows: { type: Array, default: () => [] },
+ itemColumns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
+
+<style scoped>
+ .asn-order-log-detail-scroll {
+ height: calc(100vh - 120px);
+ }
+</style>
diff --git a/rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js b/rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js
new file mode 100644
index 0000000..0edd1f6
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js
@@ -0,0 +1,260 @@
+export const ASN_ORDER_REPORT_TITLE = '鍏ュ簱閫氱煡鍗曟姤琛�'
+export const ASN_ORDER_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const ASN_ORDER_STATUS_MAP = {
+ 0: { label: '鏈墽琛�', tagType: 'info' },
+ 1: { label: '鎵ц涓�', tagType: 'warning' },
+ 2: { label: '鏀惰揣瀹屾垚', tagType: 'success' },
+ 3: { label: '浠诲姟鎵ц涓�', tagType: 'warning' },
+ 4: { label: '宸插畬鎴�', tagType: 'success' },
+ 8: { label: '鍙栨秷', tagType: 'danger' },
+ 9: { label: '宸插叧闂�', tagType: 'info' }
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+function normalizeNullableNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return '-'
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : '-'
+}
+
+function getStatusConfig(status, statusText) {
+ const numericStatus = Number(status)
+ const fallback = ASN_ORDER_STATUS_MAP[numericStatus] || {
+ label: statusText || '-',
+ tagType: 'info'
+ }
+ return {
+ label: statusText || fallback.label,
+ tagType: fallback.tagType
+ }
+}
+
+export function createAsnOrderSearchState() {
+ return {
+ condition: '',
+ code: '',
+ poCode: '',
+ wkType: '',
+ exceStatus: '',
+ supplierName: '',
+ purchaseUserName: ''
+ }
+}
+
+export function createPurchaseFilterSearchState() {
+ return {
+ condition: '',
+ code: '',
+ source: '',
+ supplierName: ''
+ }
+}
+
+export function buildAsnOrderSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'poCode', 'wkType', 'supplierName', 'purchaseUserName'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = Number(params.exceStatus)
+ }
+
+ return result
+}
+
+export function buildAsnOrderPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildAsnOrderSearchParams(params)
+ }
+}
+
+export function buildAsnOrderDetailQueryParams(params = {}) {
+ return {
+ orderId: params.orderId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function buildPurchaseFilterSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'source', 'supplierName'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+ return result
+}
+
+export function buildPurchaseFilterPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildPurchaseFilterSearchParams(params)
+ }
+}
+
+export function normalizeAsnOrderRow(record = {}) {
+ const statusConfig = getStatusConfig(record.exceStatus, record['exceStatus$'])
+ return {
+ ...record,
+ id: record.id ?? null,
+ code: record.code || '-',
+ poCode: record.poCode || '-',
+ wkTypeLabel: record['wkType$'] || record.wkType || '-',
+ orderTypeLabel: record['type$'] || record.type || '-',
+ exceStatusText: statusConfig.label,
+ exceStatusTagType: statusConfig.tagType,
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ purchaseOrgName: record.purchaseOrgName || '-',
+ purchaseUserName: record.purchaseUserName || '-',
+ supplierId: record.supplierId || '-',
+ supplierName: record.supplierName || '-',
+ businessTimeText: record['businessTime$'] || record.businessTime || '-',
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createByText: record['createBy$'] || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ memo: record.memo || '-',
+ canComplete: Number(record.exceStatus) === 1
+ }
+}
+
+export function normalizeAsnOrderItemRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? null,
+ orderCode: record.orderCode || '-',
+ poCode: record.poCode || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ platItemId: record.platItemId || '-',
+ splrBatch: record.splrBatch || '-',
+ splrCode: record.splrCode || '-',
+ splrName: record.splrName || record.supplierName || '-',
+ stockUnit: record.stockUnit || record.unit || '-',
+ purUnit: record.purUnit || '-',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ memo: record.memo || '-',
+ prodTimeText: record['prodTime$'] || record.prodTime || '-'
+ }
+}
+
+export function normalizePurchaseRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? null,
+ code: record.code || '-',
+ source: record.source || '-',
+ wkTypeLabel: record['wkType$'] || record.wkType || '-',
+ typeLabel: record['type$'] || record.type || '-',
+ supplierName: record.supplierName || '-',
+ purchaseUserName: record.purchaseUserName || '-',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ remainingQty: Math.max(normalizeNumber(record.anfme) - normalizeNumber(record.qty), 0),
+ exceStatusText: record['exceStatus$'] || '-',
+ businessTimeText: record['businessTime$'] || record.businessTime || '-'
+ }
+}
+
+export function normalizePurchaseItemRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? null,
+ platItemId: record.platItemId || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.matnrName || record.maktx || '-',
+ splrName: record.splrName || '-',
+ splrCode: record.splrCode || '-',
+ splrBatch: record.splrBatch || '-',
+ unit: record.unit || record.stockUnit || '-',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ nromQty: normalizeNullableNumber(record.nromQty)
+ }
+}
+
+export function buildAsnOrderPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeAsnOrderRow(record))
+}
+
+export function buildAsnOrderReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = ASN_ORDER_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: ASN_ORDER_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...ASN_ORDER_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function getAsnOrderStatusOptions() {
+ return Object.entries(ASN_ORDER_STATUS_MAP).map(([value, item]) => ({
+ label: item.label,
+ value: Number(value)
+ }))
+}
+
+export function getAsnOrderActionList(row = {}) {
+ const normalizedRow = normalizeAsnOrderRow(row)
+ return [
+ {
+ key: 'view',
+ label: '鏌ョ湅璇︽儏',
+ icon: 'ri:eye-line'
+ },
+ {
+ key: 'print',
+ label: '鎵撳嵃',
+ icon: 'ri:printer-line'
+ },
+ {
+ key: 'complete',
+ label: '瀹屾垚',
+ icon: 'ri:check-line',
+ color: 'var(--el-color-success)',
+ disabled: !normalizedRow.canComplete
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/asn-order/asnOrderTable.columns.js b/rsf-design/src/views/orders/asn-order/asnOrderTable.columns.js
new file mode 100644
index 0000000..569f7e9
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order/asnOrderTable.columns.js
@@ -0,0 +1,296 @@
+import { h } from 'vue'
+import { ElButton, ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+import { getAsnOrderActionList } from './asnOrderPage.helpers'
+
+export function createAsnOrderTableColumns({ handleActionClick }) {
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'code',
+ label: 'ASN鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'poCode',
+ label: 'PO鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '搴旀敹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'supplierName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'purchaseUserName',
+ label: '閲囪喘鍛�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鍗曟嵁鐘舵��',
+ width: 120,
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: row.exceStatusTagType || 'info', effect: 'light' },
+ () => row.exceStatusText
+ )
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: getAsnOrderActionList(row),
+ onClick: (item) => handleActionClick(item, row)
+ })
+ }
+ ]
+}
+
+export function createAsnOrderDetailItemColumns() {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platItemId',
+ label: 'PO琛屽彿',
+ width: 110
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'stockUnit',
+ label: '鍗曚綅',
+ width: 100
+ },
+ {
+ prop: 'anfme',
+ label: '搴旀敹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ }
+ ]
+}
+
+export function createAsnOrderPurchaseColumns({ handleChoosePurchase }) {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'code',
+ label: 'PO鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'source',
+ label: '鏉ユ簮',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'supplierName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'purchaseUserName',
+ label: '閲囪喘鍛�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'remainingQty',
+ label: '鍙缓鍗曟暟閲�',
+ width: 120,
+ align: 'right'
+ },
+ {
+ prop: 'exceStatusText',
+ label: 'PO鐘舵��',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleChoosePurchase(row)
+ })
+ }
+ ]
+}
+
+export function createAsnOrderPurchaseItemColumns() {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'platItemId',
+ label: 'PO琛屽彿',
+ width: 110
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100
+ },
+ {
+ prop: 'anfme',
+ label: '閲囪喘鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '宸茬敓鎴怉SN鏁伴噺',
+ width: 130,
+ align: 'right'
+ },
+ {
+ prop: 'nromQty',
+ label: '鏀惰揣鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'selectAction',
+ label: '鎿嶄綔鎻愮ず',
+ minWidth: 120,
+ formatter: () =>
+ h(
+ ElButton,
+ {
+ link: true,
+ type: 'primary',
+ class: '!px-0'
+ },
+ () => '闅� PO 鍏ㄩ噺寤哄崟'
+ )
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/asn-order/index.vue b/rsf-design/src/views/orders/asn-order/index.vue
new file mode 100644
index 0000000..1ef8bd6
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order/index.vue
@@ -0,0 +1,399 @@
+<template>
+ <div class="asn-order-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton type="primary" @click="poDialogVisible = true">鎸塒O寤哄崟</ElButton>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <AsnOrderDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :data="detailTableData"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @refresh="loadDetailResources"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+
+ <AsnOrderCreateByPoDialog v-model:visible="poDialogVisible" @success="handlePoCreateSuccess" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchAsnOrderItemPage,
+ fetchAsnOrderPage,
+ fetchCompleteAsnOrder,
+ fetchExportAsnOrderReport,
+ fetchGetAsnOrderDetail,
+ fetchGetAsnOrderMany
+ } from '@/api/asn-order'
+ import AsnOrderDetailDrawer from './modules/asn-order-detail-drawer.vue'
+ import AsnOrderCreateByPoDialog from './modules/asn-order-create-by-po-dialog.vue'
+ import {
+ createAsnOrderDetailItemColumns,
+ createAsnOrderTableColumns
+ } from './asnOrderTable.columns'
+ import {
+ ASN_ORDER_REPORT_STYLE,
+ ASN_ORDER_REPORT_TITLE,
+ buildAsnOrderDetailQueryParams,
+ buildAsnOrderPageQueryParams,
+ buildAsnOrderPrintRows,
+ buildAsnOrderReportMeta,
+ buildAsnOrderSearchParams,
+ createAsnOrderSearchState,
+ getAsnOrderStatusOptions,
+ normalizeAsnOrderItemRow,
+ normalizeAsnOrderRow
+ } from './asnOrderPage.helpers'
+
+ defineOptions({ name: 'AsnOrder' })
+
+ const userStore = useUserStore()
+ const reportTitle = ASN_ORDER_REPORT_TITLE
+ const searchForm = ref(createAsnOrderSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const detailTableData = ref([])
+ const activeOrderId = ref(null)
+ const poDialogVisible = ref(false)
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildAsnOrderSearchParams(searchForm.value))
+ const detailColumns = computed(() => createAsnOrderDetailItemColumns())
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� ASN 鍗曞彿/PO 鍗曞彿/渚涘簲鍟�'
+ }
+ },
+ {
+ label: 'ASN鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� ASN 鍗曞彿'
+ }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'poCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� PO 鍗曞彿'
+ }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�'
+ }
+ },
+ {
+ label: '鍗曟嵁鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getAsnOrderStatusOptions()
+ }
+ },
+ {
+ label: '渚涘簲鍟�',
+ key: 'supplierName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢'
+ }
+ },
+ {
+ label: '閲囪喘鍛�',
+ key: 'purchaseUserName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ噰璐憳'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ activeOrderId.value = row.id
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ await loadDetailResources()
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) {
+ return
+ }
+
+ try {
+ if (action.key === 'view') {
+ await openDetail(row)
+ return
+ }
+
+ if (action.key === 'print') {
+ await handlePrint({ ids: [row.id], pageSize: 1 })
+ return
+ }
+
+ if (action.key === 'complete') {
+ await ElMessageBox.confirm(`纭畾瀹屾垚鍏ュ簱閫氱煡鍗� ${row.code || ''} 鍚楋紵`, '瀹屾垚纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchCompleteAsnOrder(row.id)
+ ElMessage.success('鍏ュ簱閫氱煡鍗曞凡瀹屾垚')
+ await refreshData()
+ if (detailDrawerVisible.value && activeOrderId.value === row.id) {
+ await loadDetailResources()
+ }
+ }
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') {
+ return
+ }
+ ElMessage.error(error?.message || '鍏ュ簱閫氱煡鍗曟搷浣滃け璐�')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshSoft,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchAsnOrderPage,
+ apiParams: buildAsnOrderPageQueryParams(searchForm.value),
+ columnsFactory: () =>
+ createAsnOrderTableColumns({
+ handleActionClick
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeAsnOrderRow(item)) : []
+ }
+ })
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadDetailResources() {
+ if (!activeOrderId.value) {
+ return
+ }
+
+ detailLoading.value = true
+ try {
+ const [detailResponse, itemResponse] = await Promise.all([
+ guardRequestWithMessage(
+ fetchGetAsnOrderDetail(activeOrderId.value),
+ {},
+ {
+ timeoutMessage: '鍏ュ簱閫氱煡鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ ),
+ guardRequestWithMessage(
+ fetchAsnOrderItemPage(
+ buildAsnOrderDetailQueryParams({
+ orderId: activeOrderId.value,
+ current: detailPagination.current,
+ pageSize: detailPagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: detailPagination.current,
+ size: detailPagination.size
+ },
+ {
+ timeoutMessage: '鍏ュ簱閫氱煡鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
+ ])
+
+ detailData.value = normalizeAsnOrderRow(detailResponse)
+ detailTableData.value = Array.isArray(itemResponse?.records)
+ ? itemResponse.records.map((item) => normalizeAsnOrderItemRow(item))
+ : []
+ updatePaginationState(
+ detailPagination,
+ itemResponse,
+ detailPagination.current,
+ detailPagination.size
+ )
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildAsnOrderSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createAsnOrderSearchState()
+ resetSearchParams()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ detailPagination.current = 1
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ async function handlePoCreateSuccess() {
+ await refreshSoft()
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'asn-order.xlsx',
+ requestExport: (payload) =>
+ fetchExportAsnOrderReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords: async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetAsnOrderMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchAsnOrderPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0
+ ? Number(pagination.total)
+ : Number(payload?.pageSize) || 20
+ })
+ ).records
+ },
+ buildPreviewRows: (records) => buildAsnOrderPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...ASN_ORDER_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildAsnOrderReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || ASN_ORDER_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/asn-order/modules/asn-order-create-by-po-dialog.vue b/rsf-design/src/views/orders/asn-order/modules/asn-order-create-by-po-dialog.vue
new file mode 100644
index 0000000..f5339e0
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order/modules/asn-order-create-by-po-dialog.vue
@@ -0,0 +1,343 @@
+<template>
+ <ElDialog
+ :model-value="visible"
+ title="鎸塒O寤哄崟"
+ width="90%"
+ top="4vh"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex flex-col gap-4">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <div class="grid grid-cols-1 gap-4 xl:grid-cols-2">
+ <ElCard class="art-table-card" shadow="never">
+ <template #header>
+ <div class="flex items-center justify-between gap-3">
+ <div class="text-sm font-medium">鍙缓鍗� PO 鍒楄〃</div>
+ <ElButton :loading="purchaseLoading" @click="refreshPurchaseData">鍒锋柊</ElButton>
+ </div>
+ </template>
+
+ <ArtTable
+ :loading="purchaseLoading"
+ :data="purchaseData"
+ :columns="purchaseColumns"
+ :pagination="purchasePagination"
+ @pagination:size-change="handlePurchaseSizeChange"
+ @pagination:current-change="handlePurchaseCurrentChange"
+ />
+ </ElCard>
+
+ <ElCard class="art-table-card" shadow="never">
+ <template #header>
+ <div class="flex items-center justify-between gap-3">
+ <div class="flex flex-col">
+ <span class="text-sm font-medium">PO 鏄庣粏棰勮</span>
+ <span class="text-xs text-[var(--art-gray-600)]">
+ {{
+ selectedPurchase.code
+ ? `褰撳墠閫夋嫨锛�${selectedPurchase.code}`
+ : '璇峰厛鍦ㄥ乏渚ч�夋嫨涓�涓� PO 鍗�'
+ }}
+ </span>
+ </div>
+ <ElButton
+ :disabled="!selectedPurchase.id"
+ :loading="itemLoading"
+ @click="reloadSelectedPurchaseItems"
+ >
+ 鍒锋柊鏄庣粏
+ </ElButton>
+ </div>
+ </template>
+
+ <ArtTable :loading="itemLoading" :data="purchaseItems" :columns="purchaseItemColumns" />
+ </ElCard>
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="flex items-center justify-between gap-3">
+ <div class="text-xs text-[var(--art-gray-600)]">
+ {{
+ selectedPurchase.id
+ ? `灏嗘寜 PO ${selectedPurchase.code} 鐨� ${purchaseItems.length} 鏉℃槑缁嗙敓鎴愬叆搴撻�氱煡鍗昤
+ : '璇烽�夋嫨涓�涓彲寤哄崟鐨� PO 鍗�'
+ }}
+ </div>
+ <ElSpace>
+ <ElButton @click="handleVisibleChange(false)">鍙栨秷</ElButton>
+ <ElButton
+ type="primary"
+ :loading="submitLoading"
+ :disabled="!selectedPurchase.id"
+ @click="handleConfirm"
+ >
+ 鐢熸垚鍏ュ簱閫氱煡鍗�
+ </ElButton>
+ </ElSpace>
+ </div>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, ref, watch } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchCreateAsnOrderByPurchase,
+ fetchPurchaseFilterPage,
+ fetchPurchaseItemPage
+ } from '@/api/asn-order'
+ import {
+ buildPurchaseFilterPageQueryParams,
+ buildPurchaseFilterSearchParams,
+ createPurchaseFilterSearchState,
+ normalizePurchaseItemRow,
+ normalizePurchaseRow
+ } from '../asnOrderPage.helpers'
+ import {
+ createAsnOrderPurchaseColumns,
+ createAsnOrderPurchaseItemColumns
+ } from '../asnOrderTable.columns'
+
+ defineOptions({ name: 'AsnOrderCreateByPoDialog' })
+
+ // 鐪熷疄鍚庣娴佺▼:
+ // 1. /purchase/filters/page 鏌ヨ鍙缓鍗� PO
+ // 2. /purchaseItem/page 鍔犺浇 PO 鏄庣粏
+ // 3. /asnOrder/purchases/save 鐢熸垚 ASN
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false }
+ })
+
+ const emit = defineEmits(['update:visible', 'success'])
+
+ const searchForm = ref(createPurchaseFilterSearchState())
+ const selectedPurchase = ref({})
+ const purchaseItems = ref([])
+ const itemLoading = ref(false)
+ const submitLoading = ref(false)
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� PO 鍗曞彿/鏉ユ簮/渚涘簲鍟�'
+ }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� PO 鍗曞彿'
+ }
+ },
+ {
+ label: '鏉ユ簮',
+ key: 'source',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潵婧�'
+ }
+ },
+ {
+ label: '渚涘簲鍟�',
+ key: 'supplierName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢'
+ }
+ }
+ ])
+
+ async function handleChoosePurchase(row) {
+ selectedPurchase.value = row
+ await loadPurchaseItems(row.id)
+ }
+
+ const purchaseItemColumns = computed(() => createAsnOrderPurchaseItemColumns())
+
+ const {
+ data: purchaseData,
+ loading: purchaseLoading,
+ pagination: purchasePagination,
+ columns: purchaseColumns,
+ replaceSearchParams,
+ handleSizeChange: handlePurchaseSizeChange,
+ handleCurrentChange: handlePurchaseCurrentChange,
+ refreshData: refreshPurchaseData,
+ getData: getPurchaseData
+ } = useTable({
+ core: {
+ apiFn: fetchPurchaseFilterPage,
+ apiParams: buildPurchaseFilterPageQueryParams(searchForm.value),
+ immediate: false,
+ columnsFactory: () =>
+ createAsnOrderPurchaseColumns({
+ handleChoosePurchase
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizePurchaseRow(item)) : []
+ }
+ })
+
+ watch(
+ () => props.visible,
+ async (visible) => {
+ if (!visible) {
+ selectedPurchase.value = {}
+ purchaseItems.value = []
+ submitLoading.value = false
+ return
+ }
+
+ searchForm.value = createPurchaseFilterSearchState()
+ replaceSearchParams(buildPurchaseFilterSearchParams(searchForm.value))
+ await getPurchaseData()
+ }
+ )
+
+ watch(
+ () => purchaseData.value,
+ (rows) => {
+ if (!selectedPurchase.value?.id || !Array.isArray(rows)) {
+ return
+ }
+ const matched = rows.find((item) => item.id === selectedPurchase.value.id)
+ if (matched) {
+ selectedPurchase.value = matched
+ }
+ }
+ )
+
+ async function loadPurchaseItems(purchaseId) {
+ if (!purchaseId) {
+ purchaseItems.value = []
+ return
+ }
+
+ itemLoading.value = true
+ try {
+ const firstPage = await guardRequestWithMessage(
+ fetchPurchaseItemPage({
+ purchaseId,
+ current: 1,
+ pageSize: 200
+ }),
+ {
+ records: [],
+ total: 0,
+ current: 1,
+ size: 200
+ },
+ {
+ timeoutMessage: 'PO 鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+
+ const firstRecords = Array.isArray(firstPage?.records) ? firstPage.records : []
+ const total = Number(firstPage?.total || firstRecords.length)
+
+ if (total > firstRecords.length) {
+ const fullPage = await guardRequestWithMessage(
+ fetchPurchaseItemPage({
+ purchaseId,
+ current: 1,
+ pageSize: total
+ }),
+ {
+ records: firstRecords,
+ total,
+ current: 1,
+ size: total
+ },
+ {
+ timeoutMessage: 'PO 鍏ㄩ噺鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ purchaseItems.value = Array.isArray(fullPage?.records)
+ ? fullPage.records.map((item) => normalizePurchaseItemRow(item))
+ : firstRecords.map((item) => normalizePurchaseItemRow(item))
+ return
+ }
+
+ purchaseItems.value = firstRecords.map((item) => normalizePurchaseItemRow(item))
+ } finally {
+ itemLoading.value = false
+ }
+ }
+
+ async function reloadSelectedPurchaseItems() {
+ if (!selectedPurchase.value?.id) {
+ return
+ }
+ await loadPurchaseItems(selectedPurchase.value.id)
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildPurchaseFilterSearchParams(searchForm.value))
+ getPurchaseData()
+ }
+
+ async function handleReset() {
+ searchForm.value = createPurchaseFilterSearchState()
+ selectedPurchase.value = {}
+ purchaseItems.value = []
+ replaceSearchParams(buildPurchaseFilterSearchParams(searchForm.value))
+ await getPurchaseData()
+ }
+
+ async function handleConfirm() {
+ if (!selectedPurchase.value?.id) {
+ ElMessage.warning('璇峰厛閫夋嫨涓�涓� PO 鍗�')
+ return
+ }
+ if (!purchaseItems.value.length) {
+ ElMessage.warning('褰撳墠 PO 鍗曟病鏈夊彲寤哄崟鏄庣粏')
+ return
+ }
+
+ submitLoading.value = true
+ try {
+ await fetchCreateAsnOrderByPurchase({
+ purchaseId: selectedPurchase.value.id,
+ items: purchaseItems.value.map((item) => ({ ...item }))
+ })
+ ElMessage.success('宸叉牴鎹� PO 鍗曠敓鎴愬叆搴撻�氱煡鍗�')
+ emit('success')
+ handleVisibleChange(false)
+ } catch (error) {
+ ElMessage.error(error?.message || '鎸� PO 寤哄崟澶辫触')
+ } finally {
+ submitLoading.value = false
+ }
+ }
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue b/rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue
new file mode 100644
index 0000000..f9c0ebe
--- /dev/null
+++ b/rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue
@@ -0,0 +1,73 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鍏ュ簱閫氱煡鍗曡鎯�"
+ size="88%"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="ASN鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="PO鍗曞彿">{{ detail.poCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{
+ detail.orderTypeLabel || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁鐘舵��">{{
+ detail.exceStatusText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘缁勭粐">{{
+ detail.purchaseOrgName || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘鍛�">{{
+ detail.purchaseUserName || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟�">{{ detail.supplierName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴旀敹鏁伴噺">{{ detail.anfme ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸叉敹鏁伴噺">{{ detail.qty ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{
+ detail.updateTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{
+ detail.createTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="flex items-center justify-between">
+ <div class="text-sm text-[var(--art-gray-600)]"
+ >鏄庣粏娓呭崟锛堢墿鏂欑紪鐮�/鐗╂枡鍚嶇О/渚涘簲鍟嗘壒娆★級</div
+ >
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'AsnOrderDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/check-diff-item/checkDiffItemPage.helpers.js b/rsf-design/src/views/orders/check-diff-item/checkDiffItemPage.helpers.js
new file mode 100644
index 0000000..f3a4e57
--- /dev/null
+++ b/rsf-design/src/views/orders/check-diff-item/checkDiffItemPage.helpers.js
@@ -0,0 +1,91 @@
+const CHECK_DIFF_ITEM_REPORT_TITLE = '鐩樼偣宸紓鏄庣粏鎶ヨ〃'
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
+ )
+}
+
+export function createCheckDiffItemSearchState() {
+ return {
+ condition: '',
+ checkId: '',
+ orderCode: '',
+ matnrCode: '',
+ barcode: '',
+ reason: '',
+ exceStatus: ''
+ }
+}
+
+export function buildCheckDiffItemSearchParams(params = {}) {
+ return filterParams(params, ['current', 'pageSize', 'size'])
+}
+
+export function buildCheckDiffItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+function getCheckDiffItemStatusMeta(status) {
+ const normalized = Number(status)
+ if (normalized === 2) {
+ return { text: '宸插畬鎴�', type: 'success' }
+ }
+ if (normalized === 1) {
+ return { text: '澶勭悊涓�', type: 'warning' }
+ }
+ return { text: '寰呭鐞�', type: 'info' }
+}
+
+export function normalizeCheckDiffItemRow(row = {}) {
+ const statusMeta = getCheckDiffItemStatusMeta(row.exceStatus ?? row.exceStatus$)
+ const checkQty = Number(row.checkQty ?? 0)
+ const anfme = Number(row.anfme ?? 0)
+ return {
+ ...row,
+ exceStatusLabel: row.exceStatusLabel || row.exceStatus$ || statusMeta.text,
+ exceStatusTagType: row.exceStatusTagType || statusMeta.type,
+ diffQty: row.diffQty ?? Number((checkQty - anfme).toFixed(3)),
+ updateTimeText: row.updateTimeText || row.updateTime$ || row.updateTime || '-',
+ createTimeText: row.createTimeText || row.createTime$ || row.createTime || '-'
+ }
+}
+
+export function getCheckDiffItemActionList(record = {}) {
+ return [
+ { key: 'view', label: '鏌ョ湅璇︽儏', icon: 'ri:file-list-3-line' },
+ {
+ key: 'approve',
+ label: '瀹℃壒閫氳繃',
+ icon: 'ri:checkbox-circle-line',
+ auth: 'edit',
+ disabled: Number(record.exceStatus) === 2
+ }
+ ]
+}
+
+export function buildCheckDiffItemPrintRows(records = []) {
+ return Array.isArray(records) ? records.map((item) => normalizeCheckDiffItemRow(item)) : []
+}
+
+export function buildCheckDiffItemReportMeta(rows = []) {
+ return {
+ reportTitle: CHECK_DIFF_ITEM_REPORT_TITLE,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ count: rows.length
+ }
+}
+
+export { CHECK_DIFF_ITEM_REPORT_TITLE, getCheckDiffItemStatusMeta }
diff --git a/rsf-design/src/views/orders/check-diff-item/checkDiffItemTable.columns.js b/rsf-design/src/views/orders/check-diff-item/checkDiffItemTable.columns.js
new file mode 100644
index 0000000..089d6b9
--- /dev/null
+++ b/rsf-design/src/views/orders/check-diff-item/checkDiffItemTable.columns.js
@@ -0,0 +1,108 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getCheckDiffItemActionList, getCheckDiffItemStatusMeta } from './checkDiffItemPage.helpers'
+
+export function createCheckDiffItemTableColumns({ handleView, handleApprove } = {}) {
+ return [
+ { type: 'selection', width: 52, fixed: 'left' },
+ {
+ prop: 'orderCode',
+ label: '鐩樼偣鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'spec',
+ label: '瑙勬牸',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'model',
+ label: '鍨嬪彿',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'barcode',
+ label: '鎵樼洏鐮�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '甯愰潰搴撳瓨',
+ width: 110,
+ formatter: (row) => row.anfme ?? '-'
+ },
+ {
+ prop: 'checkQty',
+ label: '鐩樼偣搴撳瓨',
+ width: 110,
+ formatter: (row) => row.checkQty ?? '-'
+ },
+ {
+ prop: 'diffQty',
+ label: '宸紓鏁伴噺',
+ width: 110,
+ formatter: (row) => row.diffQty ?? '-'
+ },
+ {
+ prop: 'reason',
+ label: '宸紓鍘熷洜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'exceStatus',
+ label: '鐩樼偣鐘舵��',
+ width: 120,
+ formatter: (row) => {
+ const statusMeta = getCheckDiffItemStatusMeta(row.exceStatus ?? row.exceStatus$)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ sortable: true,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 140,
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', [
+ h(ArtButtonMore, {
+ list: getCheckDiffItemActionList(row),
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'approve') handleApprove?.(row)
+ }
+ })
+ ])
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/check-diff-item/index.vue b/rsf-design/src/views/orders/check-diff-item/index.vue
new file mode 100644
index 0000000..7364784
--- /dev/null
+++ b/rsf-design/src/views/orders/check-diff-item/index.vue
@@ -0,0 +1,224 @@
+<template>
+ <div class="check-diff-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <CheckDiffItemDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchCheckDiffItemPage,
+ fetchDeleteCheckDiffItem,
+ fetchExportCheckDiffItemReport,
+ fetchGetCheckDiffItemDetail,
+ fetchGetCheckDiffItemMany,
+ fetchUpdateCheckDiffItem
+ } from '@/api/check-diff'
+ import CheckDiffItemDetailDrawer from './modules/check-diff-item-detail-drawer.vue'
+ import {
+ CHECK_DIFF_ITEM_REPORT_TITLE,
+ buildCheckDiffItemPageQueryParams,
+ buildCheckDiffItemPrintRows,
+ buildCheckDiffItemReportMeta,
+ buildCheckDiffItemSearchParams,
+ createCheckDiffItemSearchState,
+ normalizeCheckDiffItemRow
+ } from './checkDiffItemPage.helpers'
+ import { createCheckDiffItemTableColumns } from './checkDiffItemTable.columns'
+
+ defineOptions({ name: 'CheckDiffItem' })
+
+ const userStore = useUserStore()
+ const reportTitle = CHECK_DIFF_ITEM_REPORT_TITLE
+ const searchForm = ref(createCheckDiffItemSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+
+ const reportQueryParams = computed(() => buildCheckDiffItemSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟鍙�/鐗╂枡缂栫爜/鎵樼洏鐮�' } },
+ { label: '鐩樼偣鍗旾D', key: 'checkId', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟ID' } },
+ { label: '鐩樼偣鍗曞彿', key: 'orderCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟鍙�' } },
+ { label: '鐗╂枡缂栫爜', key: 'matnrCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�' } },
+ { label: '鎵樼洏鐮�', key: 'barcode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ墭鐩樼爜' } },
+ { label: '宸紓鍘熷洜', key: 'reason', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ樊寮傚師鍥�' } },
+ {
+ label: '鐩樼偣鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '寰呭鐞�', value: 0 },
+ { label: '澶勭悊涓�', value: 1 },
+ { label: '宸插畬鎴�', value: 2 }
+ ]
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailData.value = normalizeCheckDiffItemRow(row)
+ }
+
+ async function handleApprove(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾瀹℃壒閫氳繃 ${row.orderCode || ''} / ${row.matnrCode || ''} 鍚楋紵`, '瀹℃壒纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchUpdateCheckDiffItem({ ...row, exceStatus: 2 })
+ ElMessage.success('瀹℃壒鎴愬姛')
+ await refreshData()
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '瀹℃壒澶辫触')
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ async function handleDelete(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾鍒犻櫎宸紓鏄庣粏 ${row.orderCode || ''} / ${row.matnrCode || ''} 鍚楋紵`, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteCheckDiffItem(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshData()
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) return
+ if (action.key === 'view') {
+ openDetail(row)
+ return
+ }
+ if (action.key === 'approve') {
+ await handleApprove(row)
+ return
+ }
+ if (action.key === 'delete') {
+ await handleDelete(row)
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchCheckDiffItemPage,
+ apiParams: buildCheckDiffItemPageQueryParams(searchForm.value),
+ columnsFactory: () => createCheckDiffItemTableColumns({ handleView: openDetail, handleApprove })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeCheckDiffItemRow(item)) : [])
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetCheckDiffItemMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchCheckDiffItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'check-diff-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportCheckDiffItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildCheckDiffItemPrintRows(records),
+ buildPreviewMeta: (rows) => buildCheckDiffItemReportMeta(rows)
+ })
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildCheckDiffItemSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createCheckDiffItemSearchState()
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/orders/check-diff-item/modules/check-diff-item-detail-drawer.vue b/rsf-design/src/views/orders/check-diff-item/modules/check-diff-item-detail-drawer.vue
new file mode 100644
index 0000000..f1b3e31
--- /dev/null
+++ b/rsf-design/src/views/orders/check-diff-item/modules/check-diff-item-detail-drawer.vue
@@ -0,0 +1,42 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鐩樼偣宸紓鏄庣粏璇︽儏"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="鐩樼偣鍗曞彿">{{ detail.orderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩樼偣鐘舵��">
+ <ElTag :type="detail.exceStatusTagType || 'info'" effect="light">
+ {{ detail.exceStatusLabel || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="甯愰潰搴撳瓨">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩樼偣搴撳瓨">{{ detail.checkQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸紓鏁伴噺">{{ detail.diffQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸紓鍘熷洜">{{ detail.reason || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'CheckDiffItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/check-diff/checkDiffPage.helpers.js b/rsf-design/src/views/orders/check-diff/checkDiffPage.helpers.js
new file mode 100644
index 0000000..949e94a
--- /dev/null
+++ b/rsf-design/src/views/orders/check-diff/checkDiffPage.helpers.js
@@ -0,0 +1,124 @@
+const CHECK_DIFF_REPORT_TITLE = '鐩樼偣宸紓鍗曟姤琛�'
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
+ )
+}
+
+export function buildCheckDiffPageRequestParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildCheckDiffItemPageRequestParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function createCheckDiffSearchState() {
+ return {
+ condition: '',
+ orderCode: '',
+ checkType: '',
+ exceStatus: '',
+ areaId: '',
+ areaName: '',
+ memo: ''
+ }
+}
+
+export function buildCheckDiffSearchParams(params = {}) {
+ return filterParams(params, ['current', 'pageSize', 'size'])
+}
+
+export function buildCheckDiffPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+export function buildCheckDiffDetailQueryParams(params = {}) {
+ return {
+ checkId: params.checkId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+function getCheckTypeLabel(checkType) {
+ if (checkType === 0 || checkType === '0') return '鏄庣洏'
+ if (checkType === 1 || checkType === '1') return '鏆楃洏'
+ if (checkType === null || checkType === undefined || checkType === '') return '-'
+ return String(checkType)
+}
+
+function getCheckDiffStatusMeta(status) {
+ const normalized = Number(status)
+ if (normalized === 1) {
+ return { text: '鎵ц涓�', type: 'warning' }
+ }
+ if (normalized === 2) {
+ return { text: '宸插畬鎴�', type: 'success' }
+ }
+ if (normalized === 8) {
+ return { text: '宸插彇娑�', type: 'danger' }
+ }
+ return { text: '鏈墽琛�', type: 'info' }
+}
+
+export function normalizeCheckDiffRow(row = {}) {
+ const statusMeta = getCheckDiffStatusMeta(row.exceStatus ?? row.exceStatus$)
+ return {
+ ...row,
+ checkTypeLabel: row.checkTypeLabel || row.checkType$ || getCheckTypeLabel(row.checkType),
+ exceStatusLabel: row.exceStatusLabel || row.exceStatus$ || statusMeta.text,
+ exceStatusTagType: row.exceStatusTagType || statusMeta.type,
+ createTimeText: row.createTimeText || row.createTime$ || row.createTime || '-',
+ updateTimeText: row.updateTimeText || row.updateTime$ || row.updateTime || '-'
+ }
+}
+
+export function getCheckDiffActionList(record = {}) {
+ return [
+ { key: 'view', label: '鏌ョ湅璇︽儏', icon: 'ri:file-list-3-line' },
+ { key: 'print', label: '鎵撳嵃', icon: 'ri:printer-line' },
+ {
+ key: 'delete',
+ label: '鍒犻櫎',
+ icon: 'ri:delete-bin-4-line',
+ color: '#f56c6c',
+ auth: 'delete'
+ }
+ ].filter((item) => item.key !== 'delete' || Number(record.exceStatus) === 0 || Number(record.exceStatus) === 1)
+}
+
+export function buildCheckDiffPrintRows(records = []) {
+ return Array.isArray(records) ? records.map((item) => normalizeCheckDiffRow(item)) : []
+}
+
+export function buildCheckDiffReportMeta(rows = []) {
+ return {
+ reportTitle: CHECK_DIFF_REPORT_TITLE,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ count: rows.length
+ }
+}
+
+export { CHECK_DIFF_REPORT_TITLE, getCheckDiffStatusMeta }
diff --git a/rsf-design/src/views/orders/check-diff/checkDiffTable.columns.js b/rsf-design/src/views/orders/check-diff/checkDiffTable.columns.js
new file mode 100644
index 0000000..d42acec
--- /dev/null
+++ b/rsf-design/src/views/orders/check-diff/checkDiffTable.columns.js
@@ -0,0 +1,69 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getCheckDiffActionList, getCheckDiffStatusMeta } from './checkDiffPage.helpers'
+
+export function createCheckDiffTableColumns({ handleActionClick } = {}) {
+ return [
+ { type: 'selection', width: 52, fixed: 'left' },
+ {
+ prop: 'orderCode',
+ label: '鐩樼偣鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'checkTypeLabel',
+ label: '鐩樼偣绫诲瀷',
+ width: 110,
+ formatter: (row) => row.checkTypeLabel || '-'
+ },
+ {
+ prop: 'areaName',
+ label: '搴撳尯鍚嶇О',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '搴旂洏鏁伴噺',
+ width: 110,
+ formatter: (row) => row.anfme ?? '-'
+ },
+ {
+ prop: 'checkQty',
+ label: '宸茬洏鏁伴噺',
+ width: 110,
+ formatter: (row) => row.checkQty ?? '-'
+ },
+ {
+ prop: 'exceStatus',
+ label: '鍗曟嵁鐘舵��',
+ width: 120,
+ formatter: (row) => {
+ const statusMeta = getCheckDiffStatusMeta(row.exceStatus ?? row.exceStatus$)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ sortable: true,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 140,
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', [
+ h(ArtButtonMore, {
+ list: getCheckDiffActionList(row),
+ onClick: (item) => handleActionClick?.(item, row)
+ })
+ ])
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/check-diff/index.vue b/rsf-design/src/views/orders/check-diff/index.vue
new file mode 100644
index 0000000..9cec74e
--- /dev/null
+++ b/rsf-design/src/views/orders/check-diff/index.vue
@@ -0,0 +1,281 @@
+<template>
+ <div class="check-diff-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <CheckDiffDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :items="detailItemRows"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchCheckDiffItemPage,
+ fetchCheckDiffPage,
+ fetchDeleteCheckDiff,
+ fetchExportCheckDiffReport,
+ fetchGetCheckDiffDetail,
+ fetchGetCheckDiffMany
+ } from '@/api/check-diff'
+ import CheckDiffDetailDrawer from './modules/check-diff-detail-drawer.vue'
+ import {
+ CHECK_DIFF_REPORT_TITLE,
+ buildCheckDiffDetailQueryParams,
+ buildCheckDiffPageQueryParams,
+ buildCheckDiffPrintRows,
+ buildCheckDiffReportMeta,
+ buildCheckDiffSearchParams,
+ createCheckDiffSearchState,
+ normalizeCheckDiffRow
+ } from './checkDiffPage.helpers'
+ import { createCheckDiffTableColumns } from './checkDiffTable.columns'
+ import { createCheckDiffItemTableColumns } from '../check-diff-item/checkDiffItemTable.columns'
+ import { normalizeCheckDiffItemRow } from '../check-diff-item/checkDiffItemPage.helpers'
+
+ defineOptions({ name: 'CheckDiff' })
+
+ const userStore = useUserStore()
+ const reportTitle = CHECK_DIFF_REPORT_TITLE
+ const searchForm = ref(createCheckDiffSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const detailItemRows = ref([])
+ const activeCheckDiffId = ref(null)
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildCheckDiffSearchParams(searchForm.value))
+ const detailColumns = computed(() => createCheckDiffItemTableColumns({ handleView: () => {} }))
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟鍙�/搴撳尯鍚嶇О/澶囨敞' } },
+ { label: '鐩樼偣鍗曞彿', key: 'orderCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟鍙�' } },
+ {
+ label: '鐩樼偣绫诲瀷',
+ key: 'checkType',
+ type: 'select',
+ props: { clearable: true, options: [{ label: '鏄庣洏', value: 0 }, { label: '鏆楃洏', value: 1 }] }
+ },
+ {
+ label: '鍗曟嵁鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鏈墽琛�', value: 0 },
+ { label: '鎵ц涓�', value: 1 },
+ { label: '宸插畬鎴�', value: 2 },
+ { label: '宸插彇娑�', value: 8 }
+ ]
+ }
+ },
+ { label: '搴撳尯ID', key: 'areaId', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ簱鍖篒D' } },
+ { label: '搴撳尯鍚嶇О', key: 'areaName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ簱鍖哄悕绉�' } },
+ { label: '澶囨敞', key: 'memo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' } }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function openDetail(row) {
+ activeCheckDiffId.value = row.id
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ await loadDetailResources()
+ }
+
+ async function handleDelete(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾鍒犻櫎鐩樼偣宸紓鍗� ${row.orderCode || ''} 鍚楋紵`, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteCheckDiff(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshData()
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) return
+ if (action.key === 'view') {
+ await openDetail(row)
+ return
+ }
+ if (action.key === 'print') {
+ await handlePrint({ ids: [row.id], pageSize: 1 })
+ return
+ }
+ if (action.key === 'delete') {
+ await handleDelete(row)
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchCheckDiffPage,
+ apiParams: buildCheckDiffPageQueryParams(searchForm.value),
+ columnsFactory: () => createCheckDiffTableColumns({ handleActionClick })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeCheckDiffRow(item)) : [])
+ }
+ })
+
+ async function loadDetailResources() {
+ if (!activeCheckDiffId.value) return
+ detailLoading.value = true
+ try {
+ const [detailResponse, itemResponse] = await Promise.all([
+ guardRequestWithMessage(fetchGetCheckDiffDetail(activeCheckDiffId.value), {}, { timeoutMessage: '鐩樼偣宸紓鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }),
+ guardRequestWithMessage(
+ fetchCheckDiffItemPage(
+ buildCheckDiffDetailQueryParams({
+ checkId: activeCheckDiffId.value,
+ current: detailPagination.current,
+ pageSize: detailPagination.size
+ })
+ ),
+ { records: [], total: 0, current: detailPagination.current, size: detailPagination.size },
+ { timeoutMessage: '鐩樼偣宸紓鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ ])
+ detailData.value = normalizeCheckDiffRow(detailResponse)
+ detailItemRows.value = Array.isArray(itemResponse?.records)
+ ? itemResponse.records.map((item) => normalizeCheckDiffItemRow(item))
+ : []
+ updatePaginationState(detailPagination, itemResponse, detailPagination.current, detailPagination.size)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildCheckDiffSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createCheckDiffSearchState()
+ resetSearchParams()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetCheckDiffMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchCheckDiffPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'check-diff.xlsx',
+ requestExport: (payload) =>
+ fetchExportCheckDiffReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildCheckDiffPrintRows(records),
+ buildPreviewMeta: (rows) => buildCheckDiffReportMeta(rows)
+ })
+</script>
diff --git a/rsf-design/src/views/orders/check-diff/modules/check-diff-detail-drawer.vue b/rsf-design/src/views/orders/check-diff/modules/check-diff-detail-drawer.vue
new file mode 100644
index 0000000..9df8fce
--- /dev/null
+++ b/rsf-design/src/views/orders/check-diff/modules/check-diff-detail-drawer.vue
@@ -0,0 +1,61 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鐩樼偣宸紓鍗曡鎯�"
+ size="88%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="鐩樼偣鍗曞彿">{{ detail.orderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩樼偣绫诲瀷">{{ detail.checkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯鍚嶇О">{{ detail.areaName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁鐘舵��">
+ <ElTag :type="detail.exceStatusTagType || 'info'" effect="light">
+ {{ detail.exceStatusLabel || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="甯愰潰搴撳瓨">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩樼偣搴撳瓨">{{ detail.checkQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElCard shadow="never" class="border border-[var(--art-border-color)]">
+ <template #header>
+ <div class="text-sm font-medium text-[var(--art-text-gray-800)]">鐩樼偣鏄庣粏</div>
+ </template>
+ <ArtTable
+ :loading="loading"
+ :data="items"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </ElCard>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'CheckDiffDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ items: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/check-item/checkOrderItemPage.helpers.js b/rsf-design/src/views/orders/check-item/checkOrderItemPage.helpers.js
new file mode 100644
index 0000000..8c36052
--- /dev/null
+++ b/rsf-design/src/views/orders/check-item/checkOrderItemPage.helpers.js
@@ -0,0 +1,55 @@
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+export function createCheckOrderItemSearchState() {
+ return {
+ condition: '',
+ orderCode: '',
+ matnrCode: '',
+ maktx: '',
+ barcode: ''
+ }
+}
+
+export function buildCheckOrderItemSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'orderCode', 'matnrCode', 'maktx', 'barcode'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+ return result
+}
+
+export function buildCheckOrderItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.orderId ? { orderId: params.orderId } : {}),
+ ...buildCheckOrderItemSearchParams(params)
+ }
+}
+
+export function normalizeCheckOrderItemRow(record = {}) {
+ return {
+ ...record,
+ orderId: record.orderId ?? '-',
+ orderCode: record.orderCode || '-',
+ platOrderCode: record.platOrderCode || '-',
+ matnrId: record.matnrId ?? '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ stockUnit: record.stockUnit || '-',
+ splrBatch: record.splrBatch || '-',
+ splrCode: record.splrCode || '-',
+ splrName: record.splrName || '-',
+ barcode: record.barcode || record.trackCode || '-',
+ anfme: record.anfme ?? 0,
+ workQty: record.workQty ?? 0,
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ memo: record.memo || '-'
+ }
+}
diff --git a/rsf-design/src/views/orders/check-item/checkOrderItemTable.columns.js b/rsf-design/src/views/orders/check-item/checkOrderItemTable.columns.js
new file mode 100644
index 0000000..6f6577b
--- /dev/null
+++ b/rsf-design/src/views/orders/check-item/checkOrderItemTable.columns.js
@@ -0,0 +1,80 @@
+import { h } from 'vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createCheckOrderItemTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'orderCode',
+ label: '鐩樼偣鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'stockUnit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '搴旂洏鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '宸茬洏鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'barcode',
+ label: '鎵樼洏鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/check-item/index.vue b/rsf-design/src/views/orders/check-item/index.vue
new file mode 100644
index 0000000..90a366d
--- /dev/null
+++ b/rsf-design/src/views/orders/check-item/index.vue
@@ -0,0 +1,92 @@
+<template>
+ <div class="check-order-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <CheckOrderItemDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchCheckOrderItemPage, fetchGetCheckOrderItemDetail } from '@/api/check-order'
+ import { buildCheckOrderItemPageQueryParams, buildCheckOrderItemSearchParams, createCheckOrderItemSearchState, normalizeCheckOrderItemRow } from './checkOrderItemPage.helpers'
+ import { createCheckOrderItemTableColumns } from './checkOrderItemTable.columns'
+ import CheckOrderItemDetailDrawer from './modules/check-order-item-detail-drawer.vue'
+
+ defineOptions({ name: 'CheckOrderItem' })
+
+ const searchForm = ref(createCheckOrderItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟鍙�/鐗╂枡缂栫爜' } },
+ { label: '鐩樼偣鍗曞彿', key: 'orderCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟鍙�' } },
+ { label: '鐗╂枡缂栫爜', key: 'matnrCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�' } },
+ { label: '鐗╂枡鍚嶇О', key: 'maktx', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�' } },
+ { label: '鎵樼洏鐮�', key: 'barcode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ墭鐩樼爜' } }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ const detail = await fetchGetCheckOrderItemDetail(row.id)
+ detailData.value = normalizeCheckOrderItemRow({ ...row, ...detail })
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchCheckOrderItemPage,
+ apiParams: buildCheckOrderItemPageQueryParams(searchForm.value),
+ columnsFactory: () => createCheckOrderItemTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeCheckOrderItemRow(item)) : [])
+ }
+ })
+
+ function handleSelectionChange() {}
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildCheckOrderItemSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createCheckOrderItemSearchState()
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/orders/check-item/modules/check-order-item-detail-drawer.vue b/rsf-design/src/views/orders/check-item/modules/check-order-item-detail-drawer.vue
new file mode 100644
index 0000000..2c38e83
--- /dev/null
+++ b/rsf-design/src/views/orders/check-item/modules/check-order-item-detail-drawer.vue
@@ -0,0 +1,43 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鐩樼偣鍗曟槑缁嗚鎯�"
+ size="68%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="鐩樼偣鍗旾D">{{ detail.orderId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩樼偣鍗曞彿">{{ detail.orderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴鍗曞彿">{{ detail.platOrderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.stockUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴旂洏鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸茬洏鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗙紪鐮�">{{ detail.splrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟�">{{ detail.splrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'CheckOrderItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/check/checkOrderPage.helpers.js b/rsf-design/src/views/orders/check/checkOrderPage.helpers.js
new file mode 100644
index 0000000..1eecf03
--- /dev/null
+++ b/rsf-design/src/views/orders/check/checkOrderPage.helpers.js
@@ -0,0 +1,135 @@
+export const CHECK_ORDER_REPORT_TITLE = '鐩樼偣鍗曟姤琛�'
+
+const CHECK_EXCE_STATUS_MAP = {
+ 0: { label: '鏈墽琛�', tagType: 'info' },
+ 1: { label: '鎵ц涓�', tagType: 'warning' },
+ 2: { label: '宸插畬鎴�', tagType: 'success' },
+ 8: { label: '宸插彇娑�', tagType: 'danger' }
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function getExceStatusConfig(status, statusText) {
+ const fallback = CHECK_EXCE_STATUS_MAP[Number(status)] || {
+ label: statusText || '-',
+ tagType: 'info'
+ }
+ return {
+ label: statusText || fallback.label,
+ tagType: fallback.tagType
+ }
+}
+
+export function createCheckOrderSearchState() {
+ return {
+ condition: '',
+ code: '',
+ wkType: '',
+ exceStatus: '',
+ arrTime: '',
+ memo: ''
+ }
+}
+
+export function buildCheckOrderSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'wkType', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.arrTime) {
+ result.arrTime = params.arrTime
+ }
+
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = Number(params.exceStatus)
+ }
+
+ return result
+}
+
+export function buildCheckOrderPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildCheckOrderSearchParams(params)
+ }
+}
+
+export function buildCheckOrderDetailQueryParams(params = {}) {
+ return {
+ orderId: params.orderId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function normalizeCheckOrderRow(record = {}) {
+ const exceStatusConfig = getExceStatusConfig(record.exceStatus, record['exceStatus$'])
+ return {
+ ...record,
+ code: record.code || '-',
+ wkTypeLabel: record['wkType$'] || record.wkType || '-',
+ checkTypeLabel: record.checkType$ || record.checkType || '-',
+ anfme: record.anfme ?? 0,
+ workQty: record.workQty ?? 0,
+ qty: record.qty ?? 0,
+ arrTimeText: record['arrTime$'] || record.arrTime || '-',
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createByText: record['createBy$'] || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ memo: record.memo || '-',
+ exceStatusText: exceStatusConfig.label,
+ exceStatusTagType: exceStatusConfig.tagType,
+ canCancel: Number(record.exceStatus) === 0
+ }
+}
+
+export function normalizeCheckOrderItemRow(record = {}) {
+ return {
+ ...record,
+ orderCode: record.orderCode || '-',
+ platOrderCode: record.platOrderCode || '-',
+ matnrId: record.matnrId ?? '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ stockUnit: record.stockUnit || '-',
+ splrBatch: record.splrBatch || '-',
+ splrCode: record.splrCode || '-',
+ splrName: record.splrName || '-',
+ barcode: record.barcode || record.trackCode || '-',
+ anfme: record.anfme ?? 0,
+ workQty: record.workQty ?? 0,
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-'
+ }
+}
+
+export function getCheckOrderActionList(row = {}) {
+ const normalizedRow = normalizeCheckOrderRow(row)
+ return [
+ {
+ key: 'view',
+ label: '鏌ョ湅璇︽儏',
+ icon: 'ri:eye-line'
+ },
+ {
+ key: 'print',
+ label: '鎵撳嵃',
+ icon: 'ri:printer-line'
+ },
+ {
+ key: 'cancel',
+ label: '鍙栨秷鍗曟嵁',
+ icon: 'ri:close-circle-line',
+ color: 'var(--el-color-danger)',
+ disabled: !normalizedRow.canCancel
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/check/checkOrderTable.columns.js b/rsf-design/src/views/orders/check/checkOrderTable.columns.js
new file mode 100644
index 0000000..32138de
--- /dev/null
+++ b/rsf-design/src/views/orders/check/checkOrderTable.columns.js
@@ -0,0 +1,134 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getCheckOrderActionList } from './checkOrderPage.helpers'
+
+export function createCheckOrderTableColumns({ handleActionClick }) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: '鐩樼偣鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '鐩樼偣绫诲瀷',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'checkTypeLabel',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '搴旂洏鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '宸茬洏鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '纭鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'arrTimeText',
+ label: '鐩樼偣鏃堕棿',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鍗曟嵁鐘舵��',
+ width: 120,
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: row.exceStatusTagType || 'info', effect: 'light' },
+ () => row.exceStatusText
+ )
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: getCheckOrderActionList(row),
+ onClick: (item) => handleActionClick(item, row)
+ })
+ }
+ ]
+}
+
+export function createCheckOrderDetailItemColumns() {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'orderCode',
+ label: '鐩樼偣鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'stockUnit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '搴旂洏鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '宸茬洏鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/check/index.vue b/rsf-design/src/views/orders/check/index.vue
new file mode 100644
index 0000000..e16fe5b
--- /dev/null
+++ b/rsf-design/src/views/orders/check/index.vue
@@ -0,0 +1,249 @@
+<template>
+ <div class="check-order-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <CheckOrderDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :data="detailTableData"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchCancelCheckOrder,
+ fetchCheckOrderItemPage,
+ fetchCheckOrderPage,
+ fetchExportCheckOrderReport,
+ fetchGetCheckOrderDetail,
+ fetchGetCheckOrderMany
+ } from '@/api/check-order'
+ import CheckOrderDetailDrawer from './modules/check-order-detail-drawer.vue'
+ import {
+ CHECK_ORDER_REPORT_TITLE,
+ buildCheckOrderDetailQueryParams,
+ buildCheckOrderPageQueryParams,
+ buildCheckOrderSearchParams,
+ createCheckOrderSearchState,
+ normalizeCheckOrderItemRow,
+ normalizeCheckOrderRow
+ } from './checkOrderPage.helpers'
+ import {
+ createCheckOrderDetailItemColumns,
+ createCheckOrderTableColumns
+ } from './checkOrderTable.columns'
+
+ defineOptions({ name: 'CheckOrder' })
+
+ const userStore = useUserStore()
+ const reportTitle = CHECK_ORDER_REPORT_TITLE
+ const searchForm = ref(createCheckOrderSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const detailTableData = ref([])
+ const activeOrderId = ref(null)
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildCheckOrderSearchParams(searchForm.value))
+ const detailColumns = computed(() => createCheckOrderDetailItemColumns())
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟鍙�/澶囨敞' } },
+ { label: '鐩樼偣鍗曞彿', key: 'code', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐瑰崟鍙�' } },
+ { label: '鐩樼偣绫诲瀷', key: 'wkType', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洏鐐圭被鍨�' } },
+ {
+ label: '鍗曟嵁鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: { clearable: true, options: [{ label: '鏈墽琛�', value: 0 }, { label: '鎵ц涓�', value: 1 }, { label: '宸插畬鎴�', value: 2 }, { label: '宸插彇娑�', value: 8 }] }
+ },
+ { label: '鐩樼偣鏃ユ湡', key: 'arrTime', type: 'date', props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' } },
+ { label: '澶囨敞', key: 'memo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' } }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function openDetail(row) {
+ activeOrderId.value = row.id
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ await loadDetailResources()
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) return
+ try {
+ if (action.key === 'view') {
+ await openDetail(row)
+ return
+ }
+ if (action.key === 'print') {
+ await handlePrint({ ids: [row.id], pageSize: 1 })
+ return
+ }
+ if (action.key === 'cancel') {
+ await ElMessageBox.confirm(`纭畾鍙栨秷鐩樼偣鍗� ${row.code || ''} 鍚楋紵`, '鍙栨秷纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchCancelCheckOrder(row.id)
+ ElMessage.success('鐩樼偣鍗曞凡鍙栨秷')
+ await refreshData()
+ }
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '鐩樼偣鍗曟搷浣滃け璐�')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchCheckOrderPage,
+ apiParams: buildCheckOrderPageQueryParams(searchForm.value),
+ columnsFactory: () => createCheckOrderTableColumns({ handleActionClick })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeCheckOrderRow(item)) : [])
+ }
+ })
+
+ async function loadDetailResources() {
+ if (!activeOrderId.value) return
+ detailLoading.value = true
+ try {
+ const [detailResponse, itemResponse] = await Promise.all([
+ guardRequestWithMessage(fetchGetCheckOrderDetail(activeOrderId.value), {}, { timeoutMessage: '鐩樼偣鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }),
+ guardRequestWithMessage(
+ fetchCheckOrderItemPage(buildCheckOrderDetailQueryParams({ orderId: activeOrderId.value, current: detailPagination.current, pageSize: detailPagination.size })),
+ { records: [], total: 0, current: detailPagination.current, size: detailPagination.size },
+ { timeoutMessage: '鐩樼偣鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ ])
+ detailData.value = normalizeCheckOrderRow(detailResponse)
+ detailTableData.value = Array.isArray(itemResponse?.records) ? itemResponse.records.map((item) => normalizeCheckOrderItemRow(item)) : []
+ updatePaginationState(detailPagination, itemResponse, detailPagination.current, detailPagination.size)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildCheckOrderSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createCheckOrderSearchState()
+ resetSearchParams()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetCheckOrderMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(await fetchCheckOrderPage({ ...reportQueryParams.value, current: 1, pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20 })).records
+ }
+
+ const { previewVisible, previewRows, previewMeta: resolvedPreviewMeta, handlePreviewVisibleChange, handleExport, handlePrint } = usePrintExportPage({
+ downloadFileName: 'check-order.xlsx',
+ requestExport: (payload) => fetchExportCheckOrderReport(payload, { headers: { Authorization: userStore.accessToken || '' } }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => records.map((item) => normalizeCheckOrderRow(item)),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+</script>
diff --git a/rsf-design/src/views/orders/check/modules/check-order-detail-drawer.vue b/rsf-design/src/views/orders/check/modules/check-order-detail-drawer.vue
new file mode 100644
index 0000000..f862671
--- /dev/null
+++ b/rsf-design/src/views/orders/check/modules/check-order-detail-drawer.vue
@@ -0,0 +1,71 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鐩樼偣鍗曡鎯�"
+ size="88%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="check-order-detail-scroll">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="鐩樼偣鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩樼偣绫诲瀷">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.checkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁鐘舵��">
+ <ElTag :type="detail.exceStatusTagType || 'info'" effect="light">
+ {{ detail.exceStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="搴旂洏鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸茬洏鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="纭鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩樼偣鏃堕棿">{{ detail.arrTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElCard shadow="never" class="border border-[var(--art-border-color)]">
+ <template #header>
+ <div class="text-sm font-medium text-[var(--art-text-gray-800)]">鐗╂枡缂栫爜鏄庣粏</div>
+ </template>
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </ElCard>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'CheckOrderDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
+
+<style scoped>
+ .check-order-detail-scroll {
+ height: calc(100vh - 120px);
+ }
+</style>
diff --git a/rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js b/rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js
new file mode 100644
index 0000000..c38a44f
--- /dev/null
+++ b/rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js
@@ -0,0 +1,146 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const DELIVERY_ITEM_REPORT_TITLE = 'DO鍗曟槑缁嗘姤琛�'
+export const DELIVERY_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function createDeliveryItemSearchState() {
+ return {
+ condition: '',
+ deliveryCode: '',
+ platItemId: '',
+ matnrCode: '',
+ maktx: '',
+ splrName: '',
+ splrBatch: ''
+ }
+}
+
+export function buildDeliveryItemSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'deliveryCode', 'platItemId', 'matnrCode', 'maktx', 'splrName', 'splrCode', 'splrBatch', 'memo'].forEach(
+ (key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ }
+ )
+
+ if (params.deliveryId !== '' && params.deliveryId !== undefined && params.deliveryId !== null) {
+ result.deliveryId = normalizeNumber(params.deliveryId)
+ }
+
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ result.status = normalizeNumber(params.status)
+ }
+
+ return result
+}
+
+export function buildDeliveryItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildDeliveryItemSearchParams(params)
+ }
+}
+
+export function buildDeliveryItemDetailQueryParams(params = {}) {
+ return {
+ deliveryItemId: params.deliveryItemId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function normalizeDeliveryItemRow(record = {}) {
+ const statusMeta = normalizeStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ id: record.id ?? null,
+ deliveryId: record.deliveryId ?? '--',
+ deliveryCode: normalizeText(record.deliveryCode) || '--',
+ platItemId: normalizeText(record.platItemId) || '--',
+ matnrCode: normalizeText(record.matnrCode) || '--',
+ maktx: normalizeText(record.maktx || record.matnrName) || '--',
+ matnrName: normalizeText(record.maktx || record.matnrName) || '--',
+ fieldsIndex: normalizeText(record.fieldsIndex) || '--',
+ unit: normalizeText(record.unit) || '--',
+ anfme: record.anfme ?? '--',
+ workQty: record.workQty ?? '--',
+ qty: record.qty ?? '--',
+ nromQty: record.nromQty ?? '--',
+ printQty: record.printQty ?? '--',
+ splrName: normalizeText(record.splrName) || '--',
+ splrCode: normalizeText(record.splrCode) || '--',
+ splrBatch: normalizeText(record.splrBatch) || '--',
+ batch: normalizeText(record.batch) || '--',
+ trackCode: normalizeText(record.trackCode) || '--',
+ packName: normalizeText(record.packName) || '--',
+ prodTimeText: normalizeText(record['prodTime$'] || record.prodTimeText || record.prodTime) || '--',
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function buildDeliveryItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeDeliveryItemRow(record))
+}
+
+export function buildDeliveryItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = DELIVERY_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: DELIVERY_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...DELIVERY_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/delivery-item/deliveryItemTable.columns.js b/rsf-design/src/views/orders/delivery-item/deliveryItemTable.columns.js
new file mode 100644
index 0000000..47eea48
--- /dev/null
+++ b/rsf-design/src/views/orders/delivery-item/deliveryItemTable.columns.js
@@ -0,0 +1,141 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createDeliveryItemTableColumns({ handleActionClick } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'deliveryCode',
+ label: '浜ゆ帴鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.deliveryCode || '--'
+ },
+ {
+ prop: 'deliveryId',
+ label: '涓诲崟ID',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.deliveryId ?? '--'
+ },
+ {
+ prop: 'platItemId',
+ label: '骞冲彴琛屽彿',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platItemId || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '鍔ㄦ�佸瓧娈电储寮�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.fieldsIndex || '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸插嚭鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'nromQty',
+ label: '鏍囧噯鍖呰',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.nromQty ?? '--'
+ },
+ {
+ prop: 'printQty',
+ label: '鎵撳嵃鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.printQty ?? '--'
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟嗗悕绉�',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrName || '--'
+ },
+ {
+ prop: 'splrCode',
+ label: '渚涘簲鍟嗙紪鐮�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrCode || '--'
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrBatch || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '--')
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleActionClick?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/delivery-item/index.vue b/rsf-design/src/views/orders/delivery-item/index.vue
new file mode 100644
index 0000000..eea4e50
--- /dev/null
+++ b/rsf-design/src/views/orders/delivery-item/index.vue
@@ -0,0 +1,178 @@
+<template>
+ <div class="delivery-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <DeliveryItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchDeliveryItemPage, fetchGetDeliveryItemDetail } from '@/api/delivery'
+ import DeliveryItemDetailDrawer from './modules/delivery-item-detail-drawer.vue'
+ import { createDeliveryItemTableColumns } from './deliveryItemTable.columns.js'
+ import {
+ buildDeliveryItemPageQueryParams,
+ createDeliveryItemSearchState,
+ normalizeDeliveryItemRow
+ } from './deliveryItemPage.helpers.js'
+
+ defineOptions({ name: 'DeliveryItem' })
+
+ const searchForm = ref(createDeliveryItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ氦鎺ュ崟鍙�/鐗╂枡缂栫爜/鐗╂枡鍚嶇О'
+ }
+ },
+ {
+ label: '浜ゆ帴鍗曞彿',
+ key: 'deliveryCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ氦鎺ュ崟鍙�'
+ }
+ },
+ {
+ label: '骞冲彴琛屽彿',
+ key: 'platItemId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ钩鍙拌鍙�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗗悕绉�',
+ key: 'splrName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鍚嶇О'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ guardRequestWithMessage(fetchGetDeliveryItemDetail(row.id), {}, {
+ timeoutMessage: '浜ゆ帴鍗曟槑缁嗚鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ .then((detail) => {
+ detailData.value = normalizeDeliveryItemRow(detail)
+ })
+ .catch((error) => {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇浜ゆ帴鍗曟槑缁嗚鎯呭け璐�')
+ })
+ .finally(() => {
+ detailLoading.value = false
+ })
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchDeliveryItemPage,
+ apiParams: buildDeliveryItemPageQueryParams({
+ ...searchForm.value,
+ pageSize: 20
+ }),
+ columnsFactory: () => createDeliveryItemTableColumns({ handleActionClick: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeDeliveryItemRow(item)) : []
+ }
+ })
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createDeliveryItemSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/orders/delivery-item/modules/delivery-item-detail-drawer.vue b/rsf-design/src/views/orders/delivery-item/modules/delivery-item-detail-drawer.vue
new file mode 100644
index 0000000..6688dd8
--- /dev/null
+++ b/rsf-design/src/views/orders/delivery-item/modules/delivery-item-detail-drawer.vue
@@ -0,0 +1,74 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浜ゆ帴鍗曟槑缁嗚鎯�"
+ size="960px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="10" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="浜ゆ帴鍗曞彿">{{ detail.deliveryCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓诲崟ID">{{ detail.deliveryId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴琛屽彿">{{ detail.platItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍔ㄦ�佸瓧娈电储寮�">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸插嚭鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏍囧噯鍖呰">{{ detail.nromQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵撳嵃鏁伴噺">{{ detail.printQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗗悕绉�">{{ detail.splrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗙紪鐮�">{{ detail.splrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璺熻釜鐮�">{{ detail.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍖呰">{{ detail.packName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐢熶骇鏃ユ湡">{{ detail.prodTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ defineOptions({ name: 'DeliveryItemDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/orders/delivery/deliveryPage.helpers.js b/rsf-design/src/views/orders/delivery/deliveryPage.helpers.js
new file mode 100644
index 0000000..c770e3c
--- /dev/null
+++ b/rsf-design/src/views/orders/delivery/deliveryPage.helpers.js
@@ -0,0 +1,264 @@
+const DELIVERY_STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '绂佺敤', type: 'danger', bool: false }
+}
+
+const DELIVERY_EXCE_STATUS_META = {
+ 0: { text: '鏈墽琛�', type: 'info' },
+ 1: { text: '鎵ц涓�', type: 'warning' },
+ 2: { text: '閮ㄥ垎瀹屾垚', type: 'primary' },
+ 3: { text: '宸插畬鎴�', type: 'success' }
+}
+
+export const DELIVERY_REPORT_TITLE = 'DO鍗曟姤琛�'
+export const DELIVERY_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return DELIVERY_STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return DELIVERY_STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+function normalizeExceStatusMeta(exceStatus, exceStatusText) {
+ if (exceStatusText) {
+ const numericValue = Number(exceStatus)
+ const fallback = DELIVERY_EXCE_STATUS_META[numericValue] || {
+ text: exceStatusText,
+ type: 'info'
+ }
+ return fallback
+ }
+ return DELIVERY_EXCE_STATUS_META[Number(exceStatus)] || {
+ text: normalizeText(exceStatus) || '--',
+ type: 'info'
+ }
+}
+
+export function createDeliverySearchState() {
+ return {
+ condition: '',
+ code: '',
+ platId: '',
+ type: '',
+ wkType: '',
+ source: '',
+ exceStatus: '',
+ memo: ''
+ }
+}
+
+export function getDeliveryPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildDeliverySearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'platId', 'type', 'wkType', 'source', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = normalizeNumber(params.exceStatus)
+ }
+
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ result.status = normalizeNumber(params.status)
+ }
+
+ if (params.timeStart !== '' && params.timeStart !== undefined && params.timeStart !== null) {
+ result.timeStart = normalizeText(params.timeStart)
+ }
+
+ if (params.timeEnd !== '' && params.timeEnd !== undefined && params.timeEnd !== null) {
+ result.timeEnd = normalizeText(params.timeEnd)
+ }
+
+ return result
+}
+
+export function buildDeliveryPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildDeliverySearchParams(params)
+ }
+}
+
+export function buildDeliveryDetailQueryParams(params = {}) {
+ return {
+ deliveryId: params.deliveryId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function normalizeDeliveryRow(record = {}) {
+ const statusMeta = normalizeStatusMeta(record.statusBool ?? record.status)
+ const exceStatusMeta = normalizeExceStatusMeta(record.exceStatus, record['exceStatus$'] || record.exceStatusText)
+ return {
+ ...record,
+ id: record.id ?? null,
+ code: normalizeText(record.code) || '--',
+ platId: normalizeText(record.platId) || '--',
+ platCode: normalizeText(record.platCode) || '--',
+ source: normalizeText(record.source) || '--',
+ typeLabel: normalizeText(record['type$'] || record.type) || '--',
+ wkTypeLabel: normalizeText(record['wkType$'] || record.wkType) || '--',
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ exceStatusText: normalizeText(record['exceStatus$'] || record.exceStatusText) || exceStatusMeta.text,
+ exceStatusTagType: exceStatusMeta.type,
+ anfme: record.anfme ?? '--',
+ qty: record.qty ?? '--',
+ workQty: record.workQty ?? '--',
+ startTimeText: normalizeText(record['startTime$'] || record.startTimeText || record.startTime) || '--',
+ endTimeText: normalizeText(record['endTime$'] || record.endTimeText || record.endTime) || '--',
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function normalizeDeliveryItemRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? null,
+ deliveryId: record.deliveryId ?? '--',
+ deliveryCode: normalizeText(record.deliveryCode) || '--',
+ platOrderCode: normalizeText(record.platOrderCode) || '--',
+ platWorkCode: normalizeText(record.platWorkCode) || '--',
+ projectCode: normalizeText(record.projectCode) || '--',
+ platItemId: normalizeText(record.platItemId) || '--',
+ matnrCode: normalizeText(record.matnrCode) || '--',
+ matnrName: normalizeText(record.maktx || record.matnrName) || '--',
+ maktx: normalizeText(record.maktx || record.matnrName) || '--',
+ fieldsIndex: normalizeText(record.fieldsIndex) || '--',
+ unit: normalizeText(record.unit) || '--',
+ anfme: record.anfme ?? '--',
+ workQty: record.workQty ?? '--',
+ qty: record.qty ?? '--',
+ nromQty: record.nromQty ?? '--',
+ printQty: record.printQty ?? '--',
+ splrName: normalizeText(record.splrName) || '--',
+ splrCode: normalizeText(record.splrCode) || '--',
+ splrBatch: normalizeText(record.splrBatch) || '--',
+ batch: normalizeText(record.batch) || '--',
+ trackCode: normalizeText(record.trackCode) || '--',
+ packName: normalizeText(record.packName) || '--',
+ prodTimeText: normalizeText(record['prodTime$'] || record.prodTimeText || record.prodTime) || '--',
+ statusText: normalizeStatusMeta(record.statusBool ?? record.status).text,
+ statusType: normalizeStatusMeta(record.statusBool ?? record.status).type,
+ statusBool:
+ record.statusBool !== void 0
+ ? Boolean(record.statusBool)
+ : normalizeStatusMeta(record.statusBool ?? record.status).bool,
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function buildDeliveryPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeDeliveryRow(record))
+}
+
+export function buildDeliveryItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeDeliveryItemRow(record))
+}
+
+export function buildDeliveryReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = DELIVERY_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: DELIVERY_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...DELIVERY_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function buildDeliveryItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = DELIVERY_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: 'DO鍗曟槑缁嗘姤琛�',
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...DELIVERY_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function getDeliveryActionList(row = {}) {
+ const normalizedRow = normalizeDeliveryRow(row)
+ const actions = [
+ {
+ key: 'view',
+ label: '鏌ョ湅璇︽儏',
+ icon: 'ri:eye-line'
+ }
+ ]
+
+ if (Number(normalizedRow.exceStatus) === 0) {
+ actions.push({
+ key: 'delete',
+ label: '鍒犻櫎',
+ icon: 'ri:delete-bin-5-line',
+ color: 'var(--art-error)'
+ })
+ }
+
+ return actions
+}
diff --git a/rsf-design/src/views/orders/delivery/deliveryTable.columns.js b/rsf-design/src/views/orders/delivery/deliveryTable.columns.js
new file mode 100644
index 0000000..ae3f7e8
--- /dev/null
+++ b/rsf-design/src/views/orders/delivery/deliveryTable.columns.js
@@ -0,0 +1,122 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getDeliveryActionList } from './deliveryPage.helpers.js'
+
+export function createDeliveryTableColumns({ handleActionClick } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: '鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'platId',
+ label: 'ERP涓诲崟鏍囪瘑',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platId || '--'
+ },
+ {
+ prop: 'typeLabel',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeLabel || '--'
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.wkTypeLabel || '--'
+ },
+ {
+ prop: 'source',
+ label: '鍗曟嵁鏉ユ簮',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.source || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '搴旀敹鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '瀹炴敹鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'platCode',
+ label: '骞冲彴鍗曞彿',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platCode || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '--')
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鎵ц鐘舵��',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) =>
+ h(ElTag, { type: row.exceStatusTagType || 'info', effect: 'light' }, () => row.exceStatusText || '--')
+ },
+ {
+ prop: 'startTimeText',
+ label: '璁″垝鍑哄簱鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.startTimeText || '--'
+ },
+ {
+ prop: 'endTimeText',
+ label: '璁″垝鍑哄簱缁撴潫鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.endTimeText || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: getDeliveryActionList(row),
+ onClick: (item) => handleActionClick?.(item, row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/delivery/index.vue b/rsf-design/src/views/orders/delivery/index.vue
new file mode 100644
index 0000000..851a0d5
--- /dev/null
+++ b/rsf-design/src/views/orders/delivery/index.vue
@@ -0,0 +1,387 @@
+<template>
+ <div class="delivery-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <DeliveryDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :items-loading="detailItemsLoading"
+ :detail="detailData"
+ :item-rows="detailItemRows"
+ :pagination="detailItemPagination"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ buildDeliveryDetailQueryParams,
+ buildDeliveryPageQueryParams,
+ buildDeliveryPrintRows,
+ buildDeliveryReportMeta,
+ buildDeliverySearchParams,
+ createDeliverySearchState,
+ getDeliveryPaginationKey,
+ normalizeDeliveryItemRow,
+ normalizeDeliveryRow
+ } from './deliveryPage.helpers.js'
+ import {
+ fetchDeleteDelivery,
+ fetchDeliveryItemPage,
+ fetchDeliveryPage,
+ fetchExportDeliveryReport,
+ fetchGetDeliveryDetail,
+ fetchGetDeliveryMany
+ } from '@/api/delivery'
+ import DeliveryDetailDrawer from './modules/delivery-detail-drawer.vue'
+ import { createDeliveryTableColumns } from './deliveryTable.columns.js'
+
+ defineOptions({ name: 'Delivery' })
+
+ const userStore = useUserStore()
+ const reportTitle = 'DO鍗曟姤琛�'
+ const searchForm = ref(createDeliverySearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailItemsLoading = ref(false)
+ const detailData = ref({})
+ const detailItemRows = ref([])
+ const activeDeliveryId = ref(null)
+ const detailItemPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildDeliverySearchParams(searchForm.value))
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟鍙�/ERP涓诲崟鏍囪瘑/骞冲彴鍗曞彿'
+ }
+ },
+ {
+ label: '鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟鍙�'
+ }
+ },
+ {
+ label: 'ERP涓诲崟鏍囪瘑',
+ key: 'platId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏RP涓诲崟鏍囪瘑'
+ }
+ },
+ {
+ label: '鍗曟嵁绫诲瀷',
+ key: 'type',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟鎹被鍨�'
+ }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�'
+ }
+ },
+ {
+ label: '鍗曟嵁鏉ユ簮',
+ key: 'source',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟鎹潵婧�'
+ }
+ },
+ {
+ label: '鎵ц鐘舵��',
+ key: 'exceStatus',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ墽琛岀姸鎬�'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ async function loadDetailItems(deliveryId) {
+ if (!deliveryId) {
+ detailItemRows.value = []
+ detailItemPagination.total = 0
+ return
+ }
+
+ detailItemsLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchDeliveryItemPage(
+ buildDeliveryDetailQueryParams({
+ deliveryId,
+ current: detailItemPagination.current,
+ pageSize: detailItemPagination.size
+ })
+ ),
+ { records: [] },
+ { timeoutMessage: '浜ゆ帴鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ const normalizedResponse = defaultResponseAdapter(response)
+ detailItemRows.value = normalizedResponse.records.map((item) => normalizeDeliveryItemRow(item))
+ detailItemPagination.total = Number(normalizedResponse.total || 0)
+ detailItemPagination.current = Number(normalizedResponse.current || detailItemPagination.current || 1)
+ detailItemPagination.size = Number(normalizedResponse.size || detailItemPagination.size || 20)
+ } finally {
+ detailItemsLoading.value = false
+ }
+ }
+
+ async function loadDeliveryDetail(row) {
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(
+ fetchGetDeliveryDetail(row.id),
+ {},
+ { timeoutMessage: '浜ゆ帴鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ detailData.value = normalizeDeliveryRow(detail)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openDetail(row) {
+ activeDeliveryId.value = row.id
+ detailDrawerVisible.value = true
+ detailItemPagination.current = 1
+ detailItemRows.value = []
+ detailData.value = {}
+
+ try {
+ await loadDeliveryDetail(row)
+ await loadDetailItems(row.id)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ ElMessage.error(error?.message || '鑾峰彇浜ゆ帴鍗曡鎯呭け璐�')
+ }
+ }
+
+ async function handleDelete(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄や氦鎺ュ崟銆�${row.code || row.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteDelivery(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshRemove()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ function handleTableActionClick(action, row) {
+ if (!action) {
+ return
+ }
+ if (action.key === 'view') {
+ openDetail(row)
+ return
+ }
+ if (action.key === 'delete') {
+ handleDelete(row)
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshRemove,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchDeliveryPage,
+ apiParams: buildDeliveryPageQueryParams({
+ ...searchForm.value,
+ pageSize: 20
+ }),
+ paginationKey: getDeliveryPaginationKey(),
+ columnsFactory: () => createDeliveryTableColumns({ handleActionClick: handleTableActionClick })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeDeliveryRow(item)) : [])
+ }
+ })
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildDeliveryPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createDeliverySearchState())
+ resetSearchParams()
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetDeliveryMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchDeliveryPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta: rawPreviewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'delivery.xlsx',
+ requestExport: (payload) =>
+ fetchExportDeliveryReport(
+ Array.isArray(payload?.ids) && payload.ids.length > 0 ? reportQueryParams.value : payload,
+ {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }
+ ),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildDeliveryPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildDeliveryReportMeta({
+ previewMeta: rawPreviewMeta.value,
+ count: previewRows.value.length,
+ orientation: rawPreviewMeta.value?.reportStyle?.orientation || 'landscape'
+ })
+ )
+
+ async function handleDetailCurrentChange(current) {
+ detailItemPagination.current = current
+ await loadDetailItems(activeDeliveryId.value)
+ }
+
+ async function handleDetailSizeChange(size) {
+ detailItemPagination.size = size
+ detailItemPagination.current = 1
+ await loadDetailItems(activeDeliveryId.value)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/delivery/modules/delivery-detail-drawer.vue b/rsf-design/src/views/orders/delivery/modules/delivery-detail-drawer.vue
new file mode 100644
index 0000000..920a6ee
--- /dev/null
+++ b/rsf-design/src/views/orders/delivery/modules/delivery-detail-drawer.vue
@@ -0,0 +1,178 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浜ゆ帴鍗曡鎯�"
+ size="1180px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="浜ゆ帴鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ERP涓诲崟鏍囪瘑">{{ detail.platId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴鍗曞彿">{{ detail.platCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁鏉ユ簮">{{ detail.source || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴旀敹鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹炴敹鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц涓暟閲�">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鐘舵��">
+ <ElTag :type="detail.exceStatusTagType || 'info'" effect="light">
+ {{ detail.exceStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁″垝鍑哄簱鏃堕棿">{{ detail.startTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁″垝鍑哄簱缁撴潫鏃堕棿">{{ detail.endTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="space-y-3">
+ <div class="flex items-center justify-between">
+ <div class="text-sm font-medium text-[var(--art-gray-900)]">浜ゆ帴鍗曟槑缁�</div>
+ <ElTag effect="plain">鍏� {{ itemRows.length }} 鏉�</ElTag>
+ </div>
+ <ArtTable
+ :loading="itemsLoading"
+ :data="itemRows"
+ :columns="itemColumns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import ArtTable from '@/components/core/tables/art-table/index.vue'
+
+ defineOptions({ name: 'DeliveryDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ itemsLoading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ itemRows: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'size-change', 'current-change'])
+
+ const itemColumns = [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'deliveryCode',
+ label: '浜ゆ帴鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platItemId',
+ label: '骞冲彴琛屽彿',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '鍔ㄦ�佸瓧娈电储寮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '宸插嚭鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'nromQty',
+ label: '鏍囧噯鍖呰',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'printQty',
+ label: '鎵撳嵃鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟嗗悕绉�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrCode',
+ label: '渚涘簲鍟嗙紪鐮�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ }
+ ]
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/orders/out-stock-item/index.vue b/rsf-design/src/views/orders/out-stock-item/index.vue
new file mode 100644
index 0000000..58ecafa
--- /dev/null
+++ b/rsf-design/src/views/orders/out-stock-item/index.vue
@@ -0,0 +1,217 @@
+<template>
+ <div class="out-stock-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <OutStockItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchGetOutStockItemDetail, fetchOutStockItemPage } from '@/api/out-stock-item'
+ import OutStockItemDetailDrawer from './modules/out-stock-item-detail-drawer.vue'
+ import { createOutStockItemTableColumns } from './outStockItemTable.columns.js'
+ import {
+ buildOutStockItemPageQueryParams,
+ createOutStockItemSearchState,
+ normalizeOutStockItemRow
+ } from './outStockItemPage.helpers.js'
+
+ defineOptions({ name: 'OutStockItem' })
+
+ const searchForm = ref(createOutStockItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ嚭搴撳崟鍙�/鐗╂枡缂栫爜/鐗╂枡鍚嶇О'
+ }
+ },
+ {
+ label: '鍑哄簱鍗曞彿',
+ key: 'orderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ嚭搴撳崟鍙�'
+ }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'poCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏O鍗曞彿'
+ }
+ },
+ {
+ label: '骞冲彴琛屽彿',
+ key: 'platItemId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ钩鍙拌鍙�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '鏉″舰鐮�',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯褰㈢爜'
+ }
+ },
+ {
+ label: '瀛楁绱㈠紩',
+ key: 'fieldsIndex',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈电储寮�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ guardRequestWithMessage(fetchGetOutStockItemDetail(row.id), {}, {
+ timeoutMessage: '鍑哄簱鍗曟槑缁嗚鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ .then((detail) => {
+ detailData.value = normalizeOutStockItemRow(detail)
+ })
+ .catch((error) => {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鍑哄簱鍗曟槑缁嗚鎯呭け璐�')
+ })
+ .finally(() => {
+ detailLoading.value = false
+ })
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchOutStockItemPage,
+ apiParams: buildOutStockItemPageQueryParams({
+ ...searchForm.value,
+ pageSize: 20
+ }),
+ columnsFactory: () => createOutStockItemTableColumns({ handleActionClick: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeOutStockItemRow(item)) : []
+ }
+ })
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildOutStockItemPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createOutStockItemSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/orders/out-stock-item/modules/out-stock-item-detail-drawer.vue b/rsf-design/src/views/orders/out-stock-item/modules/out-stock-item-detail-drawer.vue
new file mode 100644
index 0000000..4829125
--- /dev/null
+++ b/rsf-design/src/views/orders/out-stock-item/modules/out-stock-item-detail-drawer.vue
@@ -0,0 +1,87 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鍑哄簱鍗曟槑缁嗚鎯�"
+ size="80%"
+ destroy-on-close
+ append-to-body
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="鍑哄簱鍗曞彿">{{ detail.orderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="PO鍗曞彿">{{ detail.poCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴琛屽彿">{{ detail.platItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴璁㈠崟鍙�">{{ detail.platOrderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴宸ュ崟鍙�">{{ detail.platWorkCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰鍙�">{{ detail.projectCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勬牸">{{ detail.spec || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍨嬪彿">{{ detail.model || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鍗曚綅">{{ detail.stockUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘鍗曚綅">{{ detail.purUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍩烘湰鍗曚綅">{{ detail.baseUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉″舰鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浜岀淮鐮�">{{ detail.qrcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍖呰鍚嶇О">{{ detail.packName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusTagType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸插嚭鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲囪喘鏁伴噺">{{ detail.purQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="闇�姹傛暟閲�">{{ detail.demandQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗙紪鐮�">{{ detail.splrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗗悕绉�">{{ detail.splrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉ユ簮浠撳簱">{{ detail.sourceWarehouseId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣浠撳簱">{{ detail.targetWarehouseId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐т富">{{ detail.ownerName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇濈鑰�">{{ detail.keeperName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇敼浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇敼鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElSkeleton>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'OutStockItemDetailDrawer' })
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ default: false
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ detail: {
+ type: Object,
+ default: () => ({})
+ }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js b/rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js
new file mode 100644
index 0000000..27cffa4
--- /dev/null
+++ b/rsf-design/src/views/orders/out-stock-item/outStockItemPage.helpers.js
@@ -0,0 +1,136 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'danger' }
+}
+
+function normalizeText(value, fallback = '--') {
+ if (value === null || value === undefined || value === '') {
+ return fallback
+ }
+ return String(value).trim() || fallback
+}
+
+function normalizeNumber(value, fallback = undefined) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isNaN(numericValue) ? fallback : numericValue
+}
+
+function pushText(result, key, value) {
+ const text = normalizeText(value, '')
+ if (text) {
+ result[key] = text
+ }
+}
+
+function pushNumber(result, key, value) {
+ const numericValue = normalizeNumber(value)
+ if (numericValue !== undefined) {
+ result[key] = numericValue
+ }
+}
+
+function getStatusMeta(status, statusText) {
+ const numericStatus = Number(status)
+ return STATUS_META[numericStatus] || {
+ text: normalizeText(statusText || status, '--'),
+ type: 'info'
+ }
+}
+
+export function createOutStockItemSearchState() {
+ return {
+ condition: '',
+ orderCode: '',
+ poCode: '',
+ platItemId: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ splrBatch: '',
+ trackCode: '',
+ barcode: '',
+ fieldsIndex: '',
+ status: ''
+ }
+}
+
+export function buildOutStockItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'orderCode',
+ 'poCode',
+ 'platItemId',
+ 'matnrCode',
+ 'maktx',
+ 'batch',
+ 'splrBatch',
+ 'trackCode',
+ 'barcode',
+ 'fieldsIndex'
+ ].forEach((key) => pushText(result, key, params[key]))
+
+ pushNumber(result, 'status', params.status)
+
+ return result
+}
+
+export function buildOutStockItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildOutStockItemSearchParams(params)
+ }
+}
+
+export function normalizeOutStockItemRow(record = {}) {
+ const statusMeta = getStatusMeta(record.status, record['status$'])
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ orderCode: normalizeText(record.orderCode),
+ poCode: normalizeText(record.poCode),
+ platItemId: normalizeText(record.platItemId),
+ platOrderCode: normalizeText(record.platOrderCode),
+ platWorkCode: normalizeText(record.platWorkCode),
+ projectCode: normalizeText(record.projectCode),
+ matnrCode: normalizeText(record.matnrCode),
+ maktx: normalizeText(record.maktx),
+ spec: normalizeText(record.spec),
+ model: normalizeText(record.model),
+ batch: normalizeText(record.batch),
+ splrCode: normalizeText(record.splrCode),
+ splrName: normalizeText(record.splrName),
+ splrBatch: normalizeText(record.splrBatch),
+ stockUnit: normalizeText(record.stockUnit),
+ purUnit: normalizeText(record.purUnit),
+ baseUnit: normalizeText(record.baseUnit),
+ trackCode: normalizeText(record.trackCode),
+ barcode: normalizeText(record.barcode),
+ qrcode: normalizeText(record.qrcode),
+ packName: normalizeText(record.packName),
+ fieldsIndex: normalizeText(record.fieldsIndex),
+ sourceWarehouseId: normalizeText(record.sourceWarehouseId),
+ targetWarehouseId: normalizeText(record.targetWarehouseId),
+ ownerName: normalizeText(record.ownerName),
+ keeperName: normalizeText(record.keeperName),
+ inStockType: normalizeText(record.inStockType),
+ statusText: statusMeta.text,
+ statusTagType: statusMeta.type,
+ anfme: record.anfme ?? '--',
+ workQty: record.workQty ?? '--',
+ qty: record.qty ?? '--',
+ purQty: record.purQty ?? '--',
+ demandQty: record.demandQty ?? '--',
+ createByText: normalizeText(record['createBy$'] || record.createByText),
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime),
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText),
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime),
+ memo: normalizeText(record.memo)
+ }
+}
diff --git a/rsf-design/src/views/orders/out-stock-item/outStockItemTable.columns.js b/rsf-design/src/views/orders/out-stock-item/outStockItemTable.columns.js
new file mode 100644
index 0000000..d12747b
--- /dev/null
+++ b/rsf-design/src/views/orders/out-stock-item/outStockItemTable.columns.js
@@ -0,0 +1,119 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createOutStockItemTableColumns({ handleActionClick } = {}) {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'orderCode',
+ label: '鍑哄簱鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.orderCode || '--'
+ },
+ {
+ prop: 'poCode',
+ label: 'PO鍗曞彿',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.poCode || '--'
+ },
+ {
+ prop: 'platItemId',
+ label: '骞冲彴琛屽彿',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platItemId || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrBatch || '--'
+ },
+ {
+ prop: 'stockUnit',
+ label: '搴撳瓨鍗曚綅',
+ width: 100,
+ align: 'center',
+ formatter: (row) => row.stockUnit || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸插嚭鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '瀛楁绱㈠紩',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.fieldsIndex || '--'
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row.statusTagType || 'info', effect: 'light' }, () => row.statusText || '--')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleActionClick?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/out-stock/index.vue b/rsf-design/src/views/orders/out-stock/index.vue
new file mode 100644
index 0000000..5f73851
--- /dev/null
+++ b/rsf-design/src/views/orders/out-stock/index.vue
@@ -0,0 +1,318 @@
+<template>
+ <div class="out-stock-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <OutStockDetailDrawer v-model:visible="detailDrawerVisible" :loading="detailLoading" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchCancelOutStock,
+ fetchCompleteOutStock,
+ fetchDeleteOutStock,
+ fetchExportOutStockReport,
+ fetchGetOutStockDetail,
+ fetchGetOutStockMany,
+ fetchOutStockPage
+ } from '@/api/out-stock'
+ import OutStockDetailDrawer from './modules/out-stock-detail-drawer.vue'
+ import {
+ OUT_STOCK_REPORT_TITLE,
+ OUT_STOCK_REPORT_STYLE,
+ buildOutStockPageQueryParams,
+ buildOutStockPrintRows,
+ buildOutStockReportMeta,
+ buildOutStockSearchParams,
+ createOutStockSearchState,
+ normalizeOutStockRow
+ } from './outStockPage.helpers'
+ import { createOutStockTableColumns } from './outStockTable.columns'
+
+ defineOptions({ name: 'OutStock' })
+
+ const userStore = useUserStore()
+ const reportTitle = OUT_STOCK_REPORT_TITLE
+ const searchForm = ref(createOutStockSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const activeOutStockId = ref(null)
+
+ const reportQueryParams = computed(() => buildOutStockSearchParams(searchForm.value))
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ嚭搴撳崟鍙�/PO鍗曞彿/瀹㈡埛' } },
+ { label: '鍑哄簱鍗曞彿', key: 'code', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ嚭搴撳崟鍙�' } },
+ { label: 'PO鍗曞彿', key: 'poCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏O鍗曞彿' } },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '閿�鍞嚭搴撳崟', value: 'sales_out' },
+ { label: '璋冩嫧鍑哄簱鍗�', value: 'transfer_out' },
+ { label: '搴撳瓨鍑哄簱鍗�', value: 'stock_out' },
+ { label: '澶囪揣鍑哄簱鍗�', value: 'pre_out' }
+ ]
+ }
+ },
+ {
+ label: '鍗曟嵁鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鍒濆鍖�', value: 10 },
+ { label: '寰呭鐞�', value: 11 },
+ { label: '鐢熸垚宸ヤ綔妗�', value: 13 },
+ { label: '浣滀笟涓�', value: 14 },
+ { label: '宸插畬鎴�', value: 15 },
+ { label: '鍙栨秷', value: 8 }
+ ]
+ }
+ },
+ {
+ label: '閲婃斁鐘舵��',
+ key: 'rleStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 0 },
+ { label: '宸查噴鏀�', value: 1 }
+ ]
+ }
+ },
+ { label: '鐗╂祦鍗曞彿', key: 'logisNo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿娴佸崟鍙�' } },
+ { label: '瀹㈡埛鍚嶇О', key: 'customerName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ鎴峰悕绉�' } },
+ { label: '閿�鍞粍缁�', key: 'saleOrgName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ラ攢鍞粍缁�' } },
+ { label: '澶囨敞', key: 'memo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' } }
+ ])
+
+ function openDetail(row) {
+ activeOutStockId.value = row.id
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ guardRequestWithMessage(fetchGetOutStockDetail(row.id), {}, { timeoutMessage: '鍑哄簱鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' })
+ .then((detail) => {
+ detailData.value = normalizeOutStockRow(detail)
+ })
+ .catch((error) => {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鍑哄簱鍗曡鎯呭け璐�')
+ })
+ .finally(() => {
+ detailLoading.value = false
+ })
+ }
+
+ async function handleComplete(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾瀹屾垚鍑哄簱鍗� ${row.code || ''} 鍚楋紵`, '瀹屾垚纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchCompleteOutStock(row.id)
+ ElMessage.success('瀹屾垚鎴愬姛')
+ await refreshData()
+ if (detailDrawerVisible.value && activeOutStockId.value === row.id) {
+ openDetail(row)
+ }
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '瀹屾垚澶辫触')
+ }
+ }
+
+ async function handleCancel(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾鍙栨秷鍑哄簱鍗� ${row.code || ''} 鍚楋紵`, '鍙栨秷纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchCancelOutStock(row.id)
+ ElMessage.success('鍙栨秷鎴愬姛')
+ await refreshData()
+ if (detailDrawerVisible.value && activeOutStockId.value === row.id) {
+ openDetail(row)
+ }
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '鍙栨秷澶辫触')
+ }
+ }
+
+ async function handleDelete(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾鍒犻櫎鍑哄簱鍗� ${row.code || ''} 鍚楋紵`, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteOutStock(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshData()
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) return
+ if (action.key === 'view') {
+ openDetail(row)
+ return
+ }
+ if (action.key === 'print') {
+ await handlePrint({ ids: [row.id], pageSize: 1 })
+ return
+ }
+ if (action.key === 'complete') {
+ await handleComplete(row)
+ return
+ }
+ if (action.key === 'cancel') {
+ await handleCancel(row)
+ return
+ }
+ if (action.key === 'delete') {
+ await handleDelete(row)
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchOutStockPage,
+ apiParams: buildOutStockPageQueryParams(searchForm.value),
+ columnsFactory: () => createOutStockTableColumns({ handleActionClick })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeOutStockRow(item)) : [])
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetOutStockMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchOutStockPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const { previewVisible, previewRows, previewMeta: rawPreviewMeta, handlePreviewVisibleChange, handleExport, handlePrint } =
+ usePrintExportPage({
+ downloadFileName: 'out-stock.xlsx',
+ requestExport: (payload) =>
+ fetchExportOutStockReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildOutStockPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...OUT_STOCK_REPORT_STYLE }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildOutStockReportMeta({
+ previewMeta: rawPreviewMeta.value,
+ count: previewRows.value.length,
+ orientation: rawPreviewMeta.value?.reportStyle?.orientation || OUT_STOCK_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildOutStockPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createOutStockSearchState()
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue b/rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue
new file mode 100644
index 0000000..feebb70
--- /dev/null
+++ b/rsf-design/src/views/orders/out-stock/modules/out-stock-detail-drawer.vue
@@ -0,0 +1,74 @@
+<template>
+ <ElDrawer
+ v-model="visibleProxy"
+ :title="'鍑哄簱鍗曡鎯�'"
+ size="720px"
+ destroy-on-close
+ append-to-body
+ >
+ <ElScrollbar class="h-full">
+ <ElSkeleton :loading="loading" animated :rows="8">
+ <div class="space-y-4">
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="鍑哄簱鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="PO鍗曞彿">{{ detail.poCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁鐘舵��">
+ <ElTag :type="detail.exceStatusTagType || 'info'">{{ detail.exceStatusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="閲婃斁鐘舵��">
+ <ElTag :type="detail.rleStatusTagType || 'info'">{{ detail.rleStatusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂祦鍗曞彿">{{ detail.logisNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟鏃堕棿">{{ detail.businessTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿�鍞粍缁�">{{ detail.saleOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿�鍞憳">{{ detail.saleUserName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛缂栫爜">{{ detail.customerId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛鍚嶇О">{{ detail.customerName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱缁勭粐">{{ detail.stockOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴斿嚭鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸插嚭鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇敼浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇敼鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElSkeleton>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ defineOptions({ name: 'OutStockDetailDrawer' })
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ default: false
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ detail: {
+ type: Object,
+ default: () => ({})
+ }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visibleProxy = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+</script>
diff --git a/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js b/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js
new file mode 100644
index 0000000..d31e6ed
--- /dev/null
+++ b/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js
@@ -0,0 +1,211 @@
+const OUT_STOCK_STATUS_META = {
+ 8: { text: '鍙栨秷', type: 'danger' },
+ 10: { text: '鍒濆鍖�', type: 'info' },
+ 11: { text: '寰呭鐞�', type: 'warning' },
+ 13: { text: '鐢熸垚宸ヤ綔妗�', type: 'primary' },
+ 14: { text: '浣滀笟涓�', type: 'warning' },
+ 15: { text: '宸插畬鎴�', type: 'success' }
+}
+
+const OUT_STOCK_RLE_STATUS_META = {
+ 0: { text: '姝e父', type: 'success' },
+ 1: { text: '宸查噴鏀�', type: 'warning' }
+}
+
+const OUT_STOCK_TYPE_META = {
+ out: '鍑哄簱鍗�'
+}
+
+export const OUT_STOCK_REPORT_TITLE = '鍑哄簱鍗曟姤琛�'
+export const OUT_STOCK_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeStatusMeta(exceStatus, exceStatusText) {
+ if (exceStatusText) {
+ const fallback = OUT_STOCK_STATUS_META[Number(exceStatus)] || {
+ text: exceStatusText,
+ type: 'info'
+ }
+ return fallback
+ }
+ return OUT_STOCK_STATUS_META[Number(exceStatus)] || {
+ text: normalizeText(exceStatus) || '--',
+ type: 'info'
+ }
+}
+
+function normalizeRleStatusMeta(rleStatus, rleStatusText) {
+ if (rleStatusText) {
+ const fallback = OUT_STOCK_RLE_STATUS_META[Number(rleStatus)] || {
+ text: rleStatusText,
+ type: 'info'
+ }
+ return fallback
+ }
+ return OUT_STOCK_RLE_STATUS_META[Number(rleStatus)] || {
+ text: normalizeText(rleStatus) || '--',
+ type: 'info'
+ }
+}
+
+export function createOutStockSearchState() {
+ return {
+ condition: '',
+ code: '',
+ poCode: '',
+ wkType: '',
+ exceStatus: '',
+ rleStatus: '',
+ logisNo: '',
+ customerName: '',
+ saleOrgName: '',
+ memo: ''
+ }
+}
+
+export function buildOutStockSearchParams(params = {}) {
+ const result = {
+ type: 'out'
+ }
+
+ ;['condition', 'code', 'poCode', 'wkType', 'logisNo', 'customerName', 'saleOrgName', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = normalizeNumber(params.exceStatus)
+ }
+
+ if (params.rleStatus !== '' && params.rleStatus !== undefined && params.rleStatus !== null) {
+ result.rleStatus = normalizeNumber(params.rleStatus)
+ }
+
+ return result
+}
+
+export function buildOutStockPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildOutStockSearchParams(params)
+ }
+}
+
+export function normalizeOutStockRow(record = {}) {
+ const statusMeta = normalizeStatusMeta(record.exceStatus, record['exceStatus$'] || record.exceStatusText)
+ const rleStatusMeta = normalizeRleStatusMeta(record.rleStatus, record['rleStatus$'] || record.rleStatusText)
+ const typeValue = normalizeText(record['type$'] || record.type)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ code: normalizeText(record.code) || '--',
+ poCode: normalizeText(record.poCode) || '--',
+ typeLabel: OUT_STOCK_TYPE_META[typeValue] || typeValue || '--',
+ wkTypeLabel: normalizeText(record['wkType$'] || record.wkType) || '--',
+ exceStatusText: statusMeta.text,
+ exceStatusTagType: statusMeta.type,
+ rleStatusText: rleStatusMeta.text,
+ rleStatusTagType: rleStatusMeta.type,
+ anfme: record.anfme ?? '--',
+ workQty: record.workQty ?? '--',
+ qty: record.qty ?? '--',
+ logisNo: normalizeText(record.logisNo) || '--',
+ saleOrgName: normalizeText(record.saleOrgName) || '--',
+ saleUserName: normalizeText(record.saleUserName) || '--',
+ businessTimeText: normalizeText(record['businessTime$'] || record.businessTimeText || record.businessTime) || '--',
+ customerId: normalizeText(record.customerId) || '--',
+ customerName: normalizeText(record.customerName) || '--',
+ stockOrgName: normalizeText(record.stockOrgName) || '--',
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--',
+ canComplete: Number(record.exceStatus) !== 15,
+ canCancel: Number(record.exceStatus) === 10,
+ canDelete: Number(record.exceStatus) !== 15
+ }
+}
+
+export function getOutStockActionList(row = {}) {
+ const normalizedRow = normalizeOutStockRow(row)
+ return [
+ {
+ key: 'view',
+ label: '鏌ョ湅璇︽儏',
+ icon: 'ri:eye-line'
+ },
+ {
+ key: 'print',
+ label: '鎵撳嵃',
+ icon: 'ri:printer-line'
+ },
+ {
+ key: 'complete',
+ label: '瀹屾垚',
+ icon: 'ri:check-line',
+ color: 'var(--el-color-success)',
+ disabled: !normalizedRow.canComplete
+ },
+ {
+ key: 'cancel',
+ label: '鍙栨秷',
+ icon: 'ri:close-circle-line',
+ color: 'var(--el-color-danger)',
+ disabled: !normalizedRow.canCancel
+ },
+ {
+ key: 'delete',
+ label: '鍒犻櫎',
+ icon: 'ri:delete-bin-6-line',
+ color: 'var(--el-color-danger)',
+ disabled: !normalizedRow.canDelete
+ }
+ ]
+}
+
+export function buildOutStockPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeOutStockRow(record))
+}
+
+export function buildOutStockReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = OUT_STOCK_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: OUT_STOCK_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...OUT_STOCK_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/out-stock/outStockTable.columns.js b/rsf-design/src/views/orders/out-stock/outStockTable.columns.js
new file mode 100644
index 0000000..088b96c
--- /dev/null
+++ b/rsf-design/src/views/orders/out-stock/outStockTable.columns.js
@@ -0,0 +1,113 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getOutStockActionList } from './outStockPage.helpers'
+
+export function createOutStockTableColumns({ handleActionClick } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: '鍑哄簱鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'poCode',
+ label: 'PO鍗曞彿',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.poCode || '--'
+ },
+ {
+ prop: 'typeLabel',
+ label: '鍑哄簱绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeLabel || '--'
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.wkTypeLabel || '--'
+ },
+ {
+ prop: 'customerName',
+ label: '瀹㈡埛',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.customerName || '--'
+ },
+ {
+ prop: 'saleOrgName',
+ label: '閿�鍞粍缁�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.saleOrgName || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '搴斿嚭鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸插嚭鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'logisNo',
+ label: '鐗╂祦鍗曞彿',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.logisNo || '--'
+ },
+ {
+ prop: 'rleStatusText',
+ label: '閲婃斁鐘舵��',
+ width: 110,
+ formatter: (row) =>
+ h(ElTag, { type: row.rleStatusTagType || 'info', effect: 'light' }, () => row.rleStatusText || '--')
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鍗曟嵁鐘舵��',
+ width: 120,
+ formatter: (row) =>
+ h(ElTag, { type: row.exceStatusTagType || 'info', effect: 'light' }, () => row.exceStatusText || '--')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: getOutStockActionList(row),
+ onClick: (item) => handleActionClick?.(item, row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/preparation-item/index.vue b/rsf-design/src/views/orders/preparation-item/index.vue
new file mode 100644
index 0000000..07a12ef
--- /dev/null
+++ b/rsf-design/src/views/orders/preparation-item/index.vue
@@ -0,0 +1,300 @@
+<template>
+ <div class="preparation-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <OutStockItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useRoute } from 'vue-router'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import {
+ fetchExportPreparationItemReport,
+ fetchGetPreparationItemDetail,
+ fetchGetPreparationItemMany,
+ fetchPreparationItemPage
+ } from '@/api/preparation-item'
+ import { createOutStockItemTableColumns } from '../out-stock-item/outStockItemTable.columns.js'
+ import OutStockItemDetailDrawer from '../out-stock-item/modules/out-stock-item-detail-drawer.vue'
+ import {
+ PREPARATION_ITEM_REPORT_STYLE,
+ PREPARATION_ITEM_REPORT_TITLE,
+ buildPreparationItemPageQueryParams,
+ buildPreparationItemPrintRows,
+ buildPreparationItemReportMeta,
+ buildPreparationItemSearchParams,
+ createPreparationItemSearchState,
+ getPreparationItemPaginationKey,
+ getPreparationItemReportColumns,
+ normalizePreparationItemRow
+ } from './preparationItemPage.helpers.js'
+
+ defineOptions({ name: 'PreparationItem' })
+
+ const route = useRoute()
+ const userStore = useUserStore()
+ const initialOrderId = route.query.orderId || route.query.id
+ const searchForm = ref(
+ createPreparationItemSearchState({
+ orderId: initialOrderId !== undefined ? Number(initialOrderId) || '' : ''
+ })
+ )
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = PREPARATION_ITEM_REPORT_TITLE
+ const reportColumns = getPreparationItemReportColumns()
+ const reportQueryParams = computed(() => buildPreparationItemSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鏂欏崟鍙�/鐗╂枡缂栫爜/鐗╂枡鍚嶇О'
+ }
+ },
+ {
+ label: '澶囨枡鍗旾D',
+ key: 'orderId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ鏂欏崟ID'
+ }
+ },
+ {
+ label: '澶囨枡鍗曞彿',
+ key: 'orderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鏂欏崟鍙�'
+ }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'poCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏O鍗曞彿'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '瀛楁绱㈠紩',
+ key: 'fieldsIndex',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈电储寮�'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetPreparationItemDetail(row.id), {}, {
+ timeoutMessage: '澶囨枡鍗曟槑缁嗚鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ detailData.value = normalizePreparationItemRow({
+ ...row,
+ ...(detail || {})
+ })
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇澶囨枡鍗曟槑缁嗚鎯呭け璐�')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchPreparationItemPage,
+ apiParams: buildPreparationItemPageQueryParams(searchForm.value),
+ paginationKey: getPreparationItemPaginationKey(),
+ columnsFactory: () => createOutStockItemTableColumns({ handleActionClick: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizePreparationItemRow(item)) : []
+ }
+ })
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildPreparationItemPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ const resetSeed =
+ initialOrderId !== undefined ? { orderId: Number(initialOrderId) || '' } : {}
+ Object.assign(searchForm.value, createPreparationItemSearchState(resetSeed))
+ resetSearchParams(buildPreparationItemPageQueryParams(createPreparationItemSearchState(resetSeed)))
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetPreparationItemMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchPreparationItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'preparation-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportPreparationItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildPreparationItemPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...PREPARATION_ITEM_REPORT_STYLE
+ }
+ })
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildPreparationItemReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || PREPARATION_ITEM_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/preparation-item/preparationItemPage.helpers.js b/rsf-design/src/views/orders/preparation-item/preparationItemPage.helpers.js
new file mode 100644
index 0000000..93afb51
--- /dev/null
+++ b/rsf-design/src/views/orders/preparation-item/preparationItemPage.helpers.js
@@ -0,0 +1,111 @@
+import {
+ buildOutStockItemSearchParams,
+ normalizeOutStockItemRow
+} from '../out-stock-item/outStockItemPage.helpers.js'
+
+export const PREPARATION_ITEM_REPORT_TITLE = '澶囨枡鍗曟槑缁嗘姤琛�'
+export const PREPARATION_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+export function createPreparationItemSearchState(seed = {}) {
+ return {
+ condition: '',
+ orderId: '',
+ orderCode: '',
+ poCode: '',
+ platItemId: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ splrBatch: '',
+ trackCode: '',
+ barcode: '',
+ fieldsIndex: '',
+ status: '',
+ ...seed
+ }
+}
+
+export function getPreparationItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildPreparationItemSearchParams(params = {}) {
+ const result = {
+ ...buildOutStockItemSearchParams(params)
+ }
+
+ if (params.orderId !== '' && params.orderId !== undefined && params.orderId !== null) {
+ const numericValue = Number(params.orderId)
+ if (!Number.isNaN(numericValue)) {
+ result.orderId = numericValue
+ }
+ }
+
+ return result
+}
+
+export function buildPreparationItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildPreparationItemSearchParams(params)
+ }
+}
+
+export function normalizePreparationItemRow(record = {}) {
+ return normalizeOutStockItemRow(record)
+}
+
+export function getPreparationItemReportColumns() {
+ return [
+ { key: 'orderCode', label: '澶囨枡鍗曞彿' },
+ { key: 'poCode', label: 'PO鍗曞彿' },
+ { key: 'platItemId', label: '骞冲彴琛屽彿' },
+ { key: 'matnrCode', label: '鐗╂枡缂栫爜' },
+ { key: 'maktx', label: '鐗╂枡鍚嶇О' },
+ { key: 'batch', label: '鎵规' },
+ { key: 'splrBatch', label: '渚涘簲鍟嗘壒娆�' },
+ { key: 'stockUnit', label: '搴撳瓨鍗曚綅' },
+ { key: 'anfme', label: '鏁伴噺' },
+ { key: 'workQty', label: '鎵ц鏁伴噺' },
+ { key: 'qty', label: '宸插嚭鏁伴噺' },
+ { key: 'fieldsIndex', label: '瀛楁绱㈠紩' },
+ { key: 'statusText', label: '鐘舵��' },
+ { key: 'updateTimeText', label: '鏇存柊鏃堕棿' },
+ { key: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildPreparationItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizePreparationItemRow(record))
+}
+
+export function buildPreparationItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = PREPARATION_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: PREPARATION_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...PREPARATION_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/preparation/index.vue b/rsf-design/src/views/orders/preparation/index.vue
new file mode 100644
index 0000000..d918f5b
--- /dev/null
+++ b/rsf-design/src/views/orders/preparation/index.vue
@@ -0,0 +1,385 @@
+<template>
+ <div class="preparation-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <PreparationDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :data="detailTableData"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @refresh="loadDetailResources"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchCancelPreparation,
+ fetchCompletePreparation,
+ fetchDeletePreparation,
+ fetchExportPreparationReport,
+ fetchGetPreparationDetail,
+ fetchGetPreparationMany,
+ fetchPreparationItemPage,
+ fetchPreparationPage
+ } from '@/api/preparation'
+ import PreparationDetailDrawer from './modules/preparation-detail-drawer.vue'
+ import { createPreparationTableColumns } from './preparationTable.columns'
+ import {
+ buildPreparationDetailQueryParams,
+ buildPreparationPageQueryParams,
+ buildPreparationPrintRows,
+ buildPreparationReportMeta,
+ buildPreparationSearchParams,
+ createPreparationDetailItemColumns,
+ createPreparationSearchState,
+ normalizePreparationItemRow,
+ normalizePreparationRow,
+ PREPARATION_REPORT_STYLE,
+ PREPARATION_REPORT_TITLE
+ } from './preparationPage.helpers'
+
+ defineOptions({ name: 'Preparation' })
+
+ const userStore = useUserStore()
+ const reportTitle = PREPARATION_REPORT_TITLE
+ const searchForm = ref(createPreparationSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const detailTableData = ref([])
+ const activePreparationId = ref(null)
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildPreparationSearchParams(searchForm.value))
+ const detailColumns = computed(() => createPreparationDetailItemColumns())
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ鏂欏崟鍙�/PO鍗曞彿/瀹㈡埛' }
+ },
+ {
+ label: '澶囨枡鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ鏂欏崟鍙�' }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'poCode',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏� PO 鍗曞彿' }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�' }
+ },
+ {
+ label: '鍗曟嵁鐘舵��',
+ key: 'exceStatus',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ョ姸鎬�' }
+ },
+ {
+ label: '閲婃斁鐘舵��',
+ key: 'rleStatus',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ラ噴鏀剧姸鎬�' }
+ },
+ {
+ label: '瀹㈡埛',
+ key: 'customerName',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ鎴�' }
+ }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function openDetail(row) {
+ activePreparationId.value = row.id
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ await loadDetailResources()
+ }
+
+ async function loadDetailResources() {
+ if (!activePreparationId.value) {
+ return
+ }
+
+ detailLoading.value = true
+ try {
+ const [detailResponse, itemResponse] = await Promise.all([
+ guardRequestWithMessage(fetchGetPreparationDetail(activePreparationId.value), {}, {
+ timeoutMessage: '澶囨枡鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }),
+ guardRequestWithMessage(
+ fetchPreparationItemPage(
+ buildPreparationDetailQueryParams({
+ orderId: activePreparationId.value,
+ current: detailPagination.current,
+ pageSize: detailPagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: detailPagination.current,
+ size: detailPagination.size
+ },
+ {
+ timeoutMessage: '澶囨枡鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
+ ])
+
+ detailData.value = normalizePreparationRow(detailResponse)
+ detailTableData.value = Array.isArray(itemResponse?.records)
+ ? itemResponse.records.map((item) => normalizePreparationItemRow(item))
+ : []
+ updatePaginationState(
+ detailPagination,
+ itemResponse,
+ detailPagination.current,
+ detailPagination.size
+ )
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) {
+ return
+ }
+
+ try {
+ if (action.key === 'view') {
+ await openDetail(row)
+ return
+ }
+
+ if (action.key === 'print') {
+ await handlePrint({ ids: [row.id], pageSize: 1 })
+ return
+ }
+
+ if (action.key === 'complete') {
+ await ElMessageBox.confirm(`纭畾瀹屾垚澶囨枡鍗� ${row.code || ''} 鍚楋紵`, '瀹屾垚纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchCompletePreparation(row.id)
+ ElMessage.success('澶囨枡鍗曞凡瀹屾垚')
+ await refreshData()
+ if (detailDrawerVisible.value && activePreparationId.value === row.id) {
+ await loadDetailResources()
+ }
+ return
+ }
+
+ if (action.key === 'cancel') {
+ await ElMessageBox.confirm(`纭畾鍙栨秷澶囨枡鍗� ${row.code || ''} 鍚楋紵`, '鍙栨秷纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchCancelPreparation(row.id)
+ ElMessage.success('澶囨枡鍗曞凡鍙栨秷')
+ await refreshData()
+ if (detailDrawerVisible.value && activePreparationId.value === row.id) {
+ await loadDetailResources()
+ }
+ return
+ }
+
+ if (action.key === 'delete') {
+ await ElMessageBox.confirm(`纭畾鍒犻櫎澶囨枡鍗� ${row.code || ''} 鍚楋紵`, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeletePreparation(row.id)
+ ElMessage.success('澶囨枡鍗曞凡鍒犻櫎')
+ await refreshData()
+ }
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') {
+ return
+ }
+ ElMessage.error(error?.message || '澶囨枡鍗曟搷浣滃け璐�')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchPreparationPage,
+ apiParams: buildPreparationPageQueryParams(searchForm.value),
+ columnsFactory: () => createPreparationTableColumns({ handleActionClick })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizePreparationRow(item)) : []
+ }
+ })
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildPreparationPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createPreparationSearchState()
+ resetSearchParams()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ detailPagination.current = 1
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'preparation.xlsx',
+ requestExport: (payload) =>
+ fetchExportPreparationReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords: async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetPreparationMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchPreparationPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0
+ ? Number(pagination.total)
+ : Number(payload?.pageSize) || 20
+ })
+ ).records
+ },
+ buildPreviewRows: (records) => buildPreparationPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...PREPARATION_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildPreparationReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || PREPARATION_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue b/rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue
new file mode 100644
index 0000000..8e2c335
--- /dev/null
+++ b/rsf-design/src/views/orders/preparation/modules/preparation-detail-drawer.vue
@@ -0,0 +1,67 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="澶囨枡鍗曡鎯�"
+ size="88%"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="澶囨枡鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="PO鍗曞彿">{{ detail.poCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁鐘舵��">
+ <ElTag :type="detail.exceStatusTagType || 'info'">{{ detail.exceStatusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="閲婃斁鐘舵��">
+ <ElTag :type="detail.rleStatusTagType || 'info'">{{ detail.rleStatusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂祦鍗曞彿">{{ detail.logisNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟鏃堕棿">{{ detail.businessTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿�鍞粍缁�">{{ detail.saleOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿�鍞憳">{{ detail.saleUserName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛缂栫爜">{{ detail.customerId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛鍚嶇О">{{ detail.customerName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱缁勭粐">{{ detail.stockOrgName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴斿嚭鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸插嚭鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="flex items-center justify-between">
+ <div class="text-sm text-[var(--art-gray-600)]">鏄庣粏娓呭崟锛堢墿鏂欑紪鐮�/鐗╂枡鍚嶇О/渚涘簲鍟嗘壒娆★級</div>
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'PreparationDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/preparation/preparationPage.helpers.js b/rsf-design/src/views/orders/preparation/preparationPage.helpers.js
new file mode 100644
index 0000000..5a42752
--- /dev/null
+++ b/rsf-design/src/views/orders/preparation/preparationPage.helpers.js
@@ -0,0 +1,250 @@
+const PREPARATION_STATUS_META = {
+ 8: { text: '鍙栨秷', type: 'danger' },
+ 10: { text: '鍒濆鍖�', type: 'info' },
+ 11: { text: '寰呭鐞�', type: 'warning' },
+ 13: { text: '鐢熸垚宸ヤ綔妗�', type: 'primary' },
+ 14: { text: '浣滀笟涓�', type: 'warning' },
+ 15: { text: '宸插畬鎴�', type: 'success' }
+}
+
+const PREPARATION_RLE_STATUS_META = {
+ 0: { text: '姝e父', type: 'success' },
+ 1: { text: '宸查噴鏀�', type: 'warning' }
+}
+
+export const PREPARATION_REPORT_TITLE = '澶囨枡鍗曟姤琛�'
+export const PREPARATION_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeStatusMeta(exceStatus, exceStatusText) {
+ if (exceStatusText) {
+ return PREPARATION_STATUS_META[Number(exceStatus)] || {
+ text: exceStatusText,
+ type: 'info'
+ }
+ }
+
+ return PREPARATION_STATUS_META[Number(exceStatus)] || {
+ text: normalizeText(exceStatus) || '--',
+ type: 'info'
+ }
+}
+
+function normalizeRleStatusMeta(rleStatus, rleStatusText) {
+ if (rleStatusText) {
+ return PREPARATION_RLE_STATUS_META[Number(rleStatus)] || {
+ text: rleStatusText,
+ type: 'info'
+ }
+ }
+
+ return PREPARATION_RLE_STATUS_META[Number(rleStatus)] || {
+ text: normalizeText(rleStatus) || '--',
+ type: 'info'
+ }
+}
+
+export function createPreparationSearchState() {
+ return {
+ condition: '',
+ code: '',
+ poCode: '',
+ wkType: '',
+ exceStatus: '',
+ rleStatus: '',
+ logisNo: '',
+ customerName: '',
+ saleOrgName: '',
+ memo: ''
+ }
+}
+
+export function buildPreparationSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'code',
+ 'poCode',
+ 'wkType',
+ 'logisNo',
+ 'customerName',
+ 'saleOrgName',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = normalizeNumber(params.exceStatus)
+ }
+
+ if (params.rleStatus !== '' && params.rleStatus !== undefined && params.rleStatus !== null) {
+ result.rleStatus = normalizeNumber(params.rleStatus)
+ }
+
+ return result
+}
+
+export function buildPreparationPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildPreparationSearchParams(params)
+ }
+}
+
+export function buildPreparationDetailQueryParams(params = {}) {
+ return {
+ orderId: Number(params.orderId) || 0,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function normalizePreparationRow(record = {}) {
+ const statusMeta = normalizeStatusMeta(
+ record.exceStatus,
+ record['exceStatus$'] || record.exceStatusText
+ )
+ const rleStatusMeta = normalizeRleStatusMeta(
+ record.rleStatus,
+ record['rleStatus$'] || record.rleStatusText
+ )
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ code: normalizeText(record.code) || '--',
+ poCode: normalizeText(record.poCode) || '--',
+ typeLabel: normalizeText(record['type$'] || record.type) || '--',
+ wkTypeLabel: normalizeText(record['wkType$'] || record.wkType) || '--',
+ exceStatusText: statusMeta.text,
+ exceStatusTagType: statusMeta.type,
+ rleStatusText: rleStatusMeta.text,
+ rleStatusTagType: rleStatusMeta.type,
+ anfme: record.anfme ?? '--',
+ workQty: record.workQty ?? '--',
+ qty: record.qty ?? '--',
+ logisNo: normalizeText(record.logisNo) || '--',
+ saleOrgName: normalizeText(record.saleOrgName) || '--',
+ saleUserName: normalizeText(record.saleUserName) || '--',
+ businessTimeText:
+ normalizeText(record['businessTime$'] || record.businessTimeText || record.businessTime) ||
+ '--',
+ customerId: normalizeText(record.customerId) || '--',
+ customerName: normalizeText(record.customerName) || '--',
+ stockOrgName: normalizeText(record.stockOrgName) || '--',
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText:
+ normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText:
+ normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--',
+ canComplete: Number(record.exceStatus) !== 15,
+ canCancel: Number(record.exceStatus) === 10,
+ canDelete: Number(record.exceStatus) !== 15
+ }
+}
+
+export function normalizePreparationItemRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? null,
+ matnrCode: normalizeText(record.matnrCode) || '--',
+ maktx: normalizeText(record.maktx) || '--',
+ splrBatch: normalizeText(record.splrBatch) || '--',
+ splrName: normalizeText(record.splrName) || '--',
+ anfme: record.anfme ?? '--',
+ qty: record.qty ?? '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function getPreparationActionList(row = {}) {
+ const normalizedRow = normalizePreparationRow(row)
+ return [
+ { key: 'view', label: '鏌ョ湅璇︽儏', icon: 'ri:eye-line' },
+ { key: 'print', label: '鎵撳嵃', icon: 'ri:printer-line' },
+ {
+ key: 'complete',
+ label: '瀹屾垚',
+ icon: 'ri:check-line',
+ color: 'var(--el-color-success)',
+ disabled: !normalizedRow.canComplete
+ },
+ {
+ key: 'cancel',
+ label: '鍙栨秷',
+ icon: 'ri:close-circle-line',
+ color: 'var(--el-color-danger)',
+ disabled: !normalizedRow.canCancel
+ },
+ {
+ key: 'delete',
+ label: '鍒犻櫎',
+ icon: 'ri:delete-bin-6-line',
+ color: 'var(--el-color-danger)',
+ disabled: !normalizedRow.canDelete
+ }
+ ]
+}
+
+export function createPreparationDetailItemColumns() {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'matnrCode', label: '鐗╂枡缂栫爜', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'maktx', label: '鐗╂枡鍚嶇О', minWidth: 180, showOverflowTooltip: true },
+ { prop: 'splrBatch', label: '渚涘簲鍟嗘壒娆�', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'splrName', label: '渚涘簲鍟�', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'anfme', label: '搴斿嚭鏁伴噺', width: 100, align: 'right' },
+ { prop: 'qty', label: '宸插嚭鏁伴噺', width: 100, align: 'right' },
+ { prop: 'memo', label: '澶囨敞', minWidth: 160, showOverflowTooltip: true }
+ ]
+}
+
+export function buildPreparationPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizePreparationRow(record))
+}
+
+export function buildPreparationReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = PREPARATION_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: PREPARATION_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...PREPARATION_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/preparation/preparationTable.columns.js b/rsf-design/src/views/orders/preparation/preparationTable.columns.js
new file mode 100644
index 0000000..40bf81f
--- /dev/null
+++ b/rsf-design/src/views/orders/preparation/preparationTable.columns.js
@@ -0,0 +1,55 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getPreparationActionList } from './preparationPage.helpers'
+
+export function createPreparationTableColumns({ handleActionClick } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'code', label: '澶囨枡鍗曞彿', minWidth: 170, showOverflowTooltip: true },
+ { prop: 'poCode', label: 'PO鍗曞彿', minWidth: 150, showOverflowTooltip: true },
+ { prop: 'typeLabel', label: '鍗曟嵁绫诲瀷', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'wkTypeLabel', label: '涓氬姟绫诲瀷', minWidth: 130, showOverflowTooltip: true },
+ { prop: 'customerName', label: '瀹㈡埛', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'saleOrgName', label: '閿�鍞粍缁�', minWidth: 150, showOverflowTooltip: true },
+ { prop: 'anfme', label: '搴斿嚭鏁伴噺', width: 100, align: 'right' },
+ { prop: 'workQty', label: '鎵ц鏁伴噺', width: 100, align: 'right' },
+ { prop: 'qty', label: '宸插嚭鏁伴噺', width: 100, align: 'right' },
+ { prop: 'logisNo', label: '鐗╂祦鍗曞彿', minWidth: 140, showOverflowTooltip: true },
+ {
+ prop: 'rleStatusText',
+ label: '閲婃斁鐘舵��',
+ width: 110,
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: row.rleStatusTagType || 'info', effect: 'light' },
+ () => row.rleStatusText || '--'
+ )
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鍗曟嵁鐘舵��',
+ width: 120,
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: row.exceStatusTagType || 'info', effect: 'light' },
+ () => row.exceStatusText || '--'
+ )
+ },
+ { prop: 'updateTimeText', label: '鏇存柊鏃堕棿', minWidth: 170, showOverflowTooltip: true },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: getPreparationActionList(row),
+ onClick: (item) => handleActionClick?.(item, row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/purchase-item/index.vue b/rsf-design/src/views/orders/purchase-item/index.vue
new file mode 100644
index 0000000..8ded304
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase-item/index.vue
@@ -0,0 +1,370 @@
+<template>
+ <div class="purchase-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <PurchaseItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import {
+ fetchExportPurchaseItemReport,
+ fetchPurchaseItemDetail,
+ fetchPurchaseItemMany,
+ fetchPurchaseItemPage
+ } from '@/api/purchase-item'
+ import PurchaseItemDetailDrawer from './modules/purchase-item-detail-drawer.vue'
+ import { createPurchaseItemTableColumns } from './purchaseItemTable.columns.js'
+ import {
+ PURCHASE_ITEM_REPORT_STYLE,
+ PURCHASE_ITEM_REPORT_TITLE,
+ buildPurchaseItemPageQueryParams,
+ buildPurchaseItemPrintRows,
+ buildPurchaseItemReportMeta,
+ buildPurchaseItemSearchParams,
+ createPurchaseItemSearchState,
+ getPurchaseItemPaginationKey,
+ normalizePurchaseItemRow
+ } from './purchaseItemPage.helpers.js'
+
+ defineOptions({ name: 'PurchaseItem' })
+
+ const userStore = useUserStore()
+ const reportTitle = PURCHASE_ITEM_REPORT_TITLE
+ const searchForm = ref(createPurchaseItemSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ富鍗曟爣璇�/鐗╂枡缂栫爜/渚涘簲鍟嗕俊鎭�'
+ }
+ },
+ {
+ label: '涓诲崟鏍囪瘑',
+ key: 'purchaseId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ富鍗曟爣璇�'
+ }
+ },
+ {
+ label: 'ERP琛屽彿',
+ key: 'platItemId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏RP琛屽彿'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'matnrName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鍗曚綅',
+ key: 'unit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟浣�'
+ }
+ },
+ {
+ label: '鏁伴噺',
+ key: 'anfme',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ暟閲�'
+ }
+ },
+ {
+ label: '宸叉敹鏁伴噺',
+ key: 'qty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ凡鏀舵暟閲�'
+ }
+ },
+ {
+ label: '鏍囧噯鍖呰',
+ key: 'nromQty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ爣鍑嗗寘瑁�'
+ }
+ },
+ {
+ label: 'ASN鍗曟嵁鏁伴噺',
+ key: 'asnQty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏SN鍗曟嵁鏁伴噺'
+ }
+ },
+ {
+ label: '鏉$爜鎵撳嵃鏁伴噺',
+ key: 'printQty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ潯鐮佹墦鍗版暟閲�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗗悕绉�',
+ key: 'splrName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鍚嶇О'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗙紪鐮�',
+ key: 'splrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢缂栫爜'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ }
+ ])
+
+ const reportQueryParams = computed(() => buildPurchaseItemSearchParams(searchForm.value))
+
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData } =
+ useTable({
+ core: {
+ apiFn: fetchPurchaseItemPage,
+ apiParams: buildPurchaseItemPageQueryParams(searchForm.value),
+ paginationKey: getPurchaseItemPaginationKey(),
+ columnsFactory: () =>
+ createPurchaseItemTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizePurchaseItemRow(item)) : []
+ }
+ })
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchPurchaseItemDetail(row.id), {}, {
+ timeoutMessage: 'PO鍗曟槑缁嗚鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ detailData.value = normalizePurchaseItemRow({
+ ...row,
+ ...(detail || {})
+ })
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇PO鍗曟槑缁嗚鎯呭け璐�')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildPurchaseItemPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createPurchaseItemSearchState())
+ resetSearchParams()
+ }
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...PURCHASE_ITEM_REPORT_STYLE
+ }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchPurchaseItemMany(payload.ids)).records
+ }
+
+ return defaultResponseAdapter(
+ await fetchPurchaseItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'purchase-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportPurchaseItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildPurchaseItemPrintRows(records),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildPurchaseItemReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || PURCHASE_ITEM_REPORT_STYLE.orientation
+ })
+ )
+
+ onMounted(() => {
+ getData()
+ })
+</script>
diff --git a/rsf-design/src/views/orders/purchase-item/modules/purchase-item-detail-drawer.vue b/rsf-design/src/views/orders/purchase-item/modules/purchase-item-detail-drawer.vue
new file mode 100644
index 0000000..8b9bd8f
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase-item/modules/purchase-item-detail-drawer.vue
@@ -0,0 +1,72 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="PO鍗曟槑缁嗚鎯�"
+ size="72%"
+ destroy-on-close
+ append-to-body
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions title="鍩虹淇℃伅" :column="3" border>
+ <ElDescriptionsItem label="鏄庣粏ID">{{ displayData.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓诲崟鏍囪瘑">{{ displayData.purchaseId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ERP琛屽彿">{{ displayData.platItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ displayData.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ displayData.matnrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ displayData.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType || 'info'" effect="light">
+ {{ displayData.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="鏁伴噺淇℃伅" :column="3" border>
+ <ElDescriptionsItem label="鏁伴噺">{{ displayData.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸叉敹鏁伴噺">{{ displayData.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏍囧噯鍖呰">{{ displayData.nromQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ASN鍗曟嵁鏁伴噺">{{ displayData.asnQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉$爜鎵撳嵃鏁伴噺">{{ displayData.printQty ?? '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="渚涘簲鍟嗕俊鎭�" :column="3" border>
+ <ElDescriptionsItem label="渚涘簲鍟嗗悕绉�">{{ displayData.splrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗙紪鐮�">{{ displayData.splrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ displayData.splrBatch || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="3" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇敼浜�">{{ displayData.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="淇敼鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElSkeleton>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import { normalizePurchaseItemRow } from '../purchaseItemPage.helpers.js'
+
+ defineOptions({ name: 'PurchaseItemDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizePurchaseItemRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/purchase-item/purchaseItemPage.helpers.js b/rsf-design/src/views/orders/purchase-item/purchaseItemPage.helpers.js
new file mode 100644
index 0000000..d7793f8
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase-item/purchaseItemPage.helpers.js
@@ -0,0 +1,183 @@
+export const PURCHASE_ITEM_REPORT_TITLE = 'PO鍗曟槑缁嗘姤琛�'
+export const PURCHASE_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+function normalizeText(value, fallback = '--') {
+ if (value === null || value === undefined || value === '') {
+ return fallback
+ }
+ const text = String(value).trim()
+ return text || fallback
+}
+
+function normalizeNumber(value, fallback = '--') {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function toSearchValue(value) {
+ if (value === '' || value === null || value === undefined) {
+ return void 0
+ }
+ const text = String(value).trim()
+ return text || void 0
+}
+
+function toSearchNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return void 0
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? void 0 : parsed
+}
+
+export function createPurchaseItemSearchState() {
+ return {
+ condition: '',
+ purchaseId: '',
+ platItemId: '',
+ matnrCode: '',
+ matnrName: '',
+ unit: '',
+ anfme: '',
+ qty: '',
+ nromQty: '',
+ asnQty: '',
+ printQty: '',
+ splrName: '',
+ splrCode: '',
+ splrBatch: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getPurchaseItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getPurchaseItemStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getPurchaseItemStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildPurchaseItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'platItemId',
+ 'matnrCode',
+ 'matnrName',
+ 'unit',
+ 'splrName',
+ 'splrCode',
+ 'splrBatch',
+ 'memo'
+ ].forEach((key) => {
+ const value = toSearchValue(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['purchaseId', 'anfme', 'qty', 'nromQty', 'asnQty', 'printQty', 'status'].forEach((key) => {
+ const value = toSearchNumber(params[key])
+ if (value !== void 0) {
+ result[key] = value
+ }
+ })
+
+ return result
+}
+
+export function buildPurchaseItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildPurchaseItemSearchParams(params)
+ }
+}
+
+export function normalizePurchaseItemRow(record = {}) {
+ const statusMeta = getPurchaseItemStatusMeta(record.statusBool ?? record.status)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ purchaseId: normalizeNumber(record.purchaseId),
+ platItemId: normalizeText(record.platItemId),
+ matnrCode: normalizeText(record.matnrCode),
+ matnrName: normalizeText(record.matnrName),
+ unit: normalizeText(record.unit),
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ nromQty: normalizeNumber(record.nromQty),
+ asnQty: normalizeNumber(record.asnQty),
+ printQty: normalizeNumber(record.printQty),
+ splrName: normalizeText(record.splrName),
+ splrCode: normalizeText(record.splrCode),
+ splrBatch: normalizeText(record.splrBatch),
+ statusText: normalizeText(record['status$'] || record.statusText || statusMeta.text),
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo),
+ createByText: normalizeText(record['createBy$'] || record.createByText || '--'),
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime || '--'),
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText || '--'),
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime || '--')
+ }
+}
+
+export function buildPurchaseItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizePurchaseItemRow(record))
+}
+
+export function buildPurchaseItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = PURCHASE_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: PURCHASE_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...PURCHASE_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/purchase-item/purchaseItemTable.columns.js b/rsf-design/src/views/orders/purchase-item/purchaseItemTable.columns.js
new file mode 100644
index 0000000..2c999a6
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase-item/purchaseItemTable.columns.js
@@ -0,0 +1,161 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+import { getPurchaseItemStatusMeta } from './purchaseItemPage.helpers.js'
+
+function renderStatus(row) {
+ const statusMeta = getPurchaseItemStatusMeta(row.statusBool ?? row.status)
+ return h(
+ ElTag,
+ {
+ type: statusMeta.type,
+ effect: 'light'
+ },
+ () => statusMeta.text
+ )
+}
+
+export function createPurchaseItemTableColumns({ handleView } = {}) {
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'purchaseId',
+ label: '涓诲崟鏍囪瘑',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'platItemId',
+ label: 'ERP琛屽彿',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrName',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 200,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'nromQty',
+ label: '鏍囧噯鍖呰',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'asnQty',
+ label: 'ASN鍗曟嵁鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'printQty',
+ label: '鏉$爜鎵撳嵃鏁伴噺',
+ width: 120,
+ align: 'right'
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟嗗悕绉�',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrCode',
+ label: '渚涘簲鍟嗙紪鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 92,
+ align: 'center',
+ formatter: renderStatus
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateByText',
+ label: '淇敼浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '淇敼鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'action',
+ label: '鎿嶄綔',
+ width: 96,
+ fixed: 'right',
+ align: 'center',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
+
+export { ArtButtonTable }
diff --git a/rsf-design/src/views/orders/purchase/index.vue b/rsf-design/src/views/orders/purchase/index.vue
new file mode 100644
index 0000000..35b855d
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase/index.vue
@@ -0,0 +1,483 @@
+<template>
+ <div class="purchase-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板PO鍗�</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="deletableSelectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <PurchaseDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :purchase-data="currentPurchaseData"
+ :type-options="typeOptions"
+ :wk-type-options="wkTypeOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <PurchaseDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :items-loading="detailItemsLoading"
+ :detail="detailData"
+ :item-rows="detailItemRows"
+ :item-columns="purchaseItemColumns"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import {
+ fetchDeletePurchase,
+ fetchExportPurchaseReport,
+ fetchPurchaseDetail,
+ fetchPurchaseItemPage,
+ fetchPurchaseMany,
+ fetchPurchasePage,
+ fetchSavePurchase,
+ fetchUpdatePurchase
+ } from '@/api/purchase'
+ import PurchaseDialog from './modules/purchase-dialog.vue'
+ import PurchaseDetailDrawer from './modules/purchase-detail-drawer.vue'
+ import { createPurchaseTableColumns } from './purchaseTable.columns'
+ import {
+ PURCHASE_REPORT_STYLE,
+ PURCHASE_REPORT_TITLE,
+ buildPurchaseDialogModel,
+ buildPurchasePageQueryParams,
+ buildPurchasePrintRows,
+ buildPurchaseReportMeta,
+ buildPurchaseSavePayload,
+ buildPurchaseSearchParams,
+ createPurchaseItemColumns,
+ createPurchaseSearchState,
+ getPurchasePaginationKey,
+ normalizePurchaseDetailRecord,
+ normalizePurchaseItemRow,
+ normalizePurchaseListRow,
+ resolveDictOptions
+ } from './purchasePage.helpers'
+
+ defineOptions({ name: 'Purchase' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const reportTitle = PURCHASE_REPORT_TITLE
+ const searchForm = ref(createPurchaseSearchState())
+ const typeOptions = ref([])
+ const wkTypeOptions = ref([])
+ const exceStatusOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailItemsLoading = ref(false)
+ const detailData = ref({})
+ const detailItemRows = ref([])
+ const purchaseItemColumns = createPurchaseItemColumns()
+ let handleDeleteAction = null
+
+ const reportQueryParams = computed(() => buildPurchaseSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏O鍗曞彿/鏉ユ簮鍗曞彿/ERP鍗曞彿'
+ }
+ },
+ {
+ label: 'PO鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏O鍗曞彿'
+ }
+ },
+ {
+ label: '鍗曟嵁绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: typeOptions.value
+ }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: wkTypeOptions.value
+ }
+ },
+ {
+ label: '鏉ユ簮鍗曞彿',
+ key: 'source',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潵婧愬崟鍙�'
+ }
+ },
+ {
+ label: '鏀惰揣閬撳彛',
+ key: 'channel',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ敹璐ч亾鍙�'
+ }
+ },
+ {
+ label: 'ERP鍗曞彿',
+ key: 'platCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏RP鍗曞彿'
+ }
+ },
+ {
+ label: '椤圭洰缂栫爜',
+ key: 'project',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ」鐩紪鐮�'
+ }
+ },
+ {
+ label: '鎵ц鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: exceStatusOptions.value
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ }
+ ])
+
+ const canDeletePurchase = (row) => Number(row?.exceStatus) === 0
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ detailItemRows.value = []
+
+ try {
+ const detail = await guardRequestWithMessage(
+ fetchPurchaseDetail(row.id),
+ {},
+ {
+ timeoutMessage: 'PO鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
+ detailData.value = normalizePurchaseDetailRecord(detail)
+ await loadDetailItems(row.id)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ detailItemRows.value = []
+ ElMessage.error(error?.message || '鑾峰彇PO鍗曡鎯呭け璐�')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(
+ fetchPurchaseDetail(row.id),
+ {},
+ {
+ timeoutMessage: 'PO鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇PO鍗曡鎯呭け璐�')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchPurchasePage,
+ apiParams: buildPurchasePageQueryParams(searchForm.value),
+ paginationKey: getPurchasePaginationKey(),
+ columnsFactory: () =>
+ createPurchaseTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
+ canEdit: hasAuth('update'),
+ canDelete: hasAuth('delete'),
+ canDeleteRow: canDeletePurchase
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizePurchaseListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentPurchaseData,
+ selectedRows,
+ handleSelectionChange: handleCrudSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete: handleCrudBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildPurchaseDialogModel(),
+ buildEditModel: (record) => buildPurchaseDialogModel(record),
+ buildSavePayload: (formData) => buildPurchaseSavePayload(formData),
+ saveRequest: fetchSavePurchase,
+ updateRequest: fetchUpdatePurchase,
+ deleteRequest: fetchDeletePurchase,
+ entityName: 'PO鍗�',
+ resolveRecordLabel: (record) => record?.code || record?.source || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const deletableSelectedRows = computed(() =>
+ selectedRows.value.filter((row) => canDeletePurchase(row))
+ )
+
+ const handleSelectionChange = (rows) => {
+ handleCrudSelectionChange(rows)
+ }
+
+ const handleBatchDelete = async () => {
+ const invalidRows = selectedRows.value.filter((row) => !canDeletePurchase(row))
+ if (invalidRows.length > 0) {
+ ElMessage.warning('浠呮墽琛岀姸鎬佷负鏈紑濮嬬殑PO鍗曞厑璁稿垹闄わ紝璇疯皟鏁村嬀閫夊悗閲嶈瘯')
+ return
+ }
+ await handleCrudBatchDelete()
+ }
+
+ async function loadTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({ current: 1, pageSize: 200, dictTypeCode: 'sys_order_type', status: 1 }),
+ { records: [] },
+ { timeoutMessage: '鍗曟嵁绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ typeOptions.value = resolveDictOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadWkTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_business_type',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '涓氬姟绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ wkTypeOptions.value = resolveDictOptions(defaultResponseAdapter(response).records, { group: 1 })
+ }
+
+ async function loadExceStatusOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_po_exce_status',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '鎵ц鐘舵�侀�夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ exceStatusOptions.value = resolveDictOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadDetailItems(purchaseId) {
+ detailItemsLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchPurchaseItemPage({ purchaseId, current: 1, pageSize: 200 }),
+ { records: [] },
+ { timeoutMessage: '閲囪喘鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ detailItemRows.value = defaultResponseAdapter(response).records.map((item) =>
+ normalizePurchaseItemRow(item)
+ )
+ } catch (error) {
+ detailItemRows.value = []
+ ElMessage.error(error?.message || '鑾峰彇閲囪喘鏄庣粏澶辫触')
+ } finally {
+ detailItemsLoading.value = false
+ }
+ }
+
+ const buildPreviewDialogMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ const response =
+ Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchPurchaseMany(payload.ids)
+ : await fetchPurchasePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0
+ ? Number(pagination.total)
+ : Number(payload?.pageSize) || 20
+ })
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'purchase.xlsx',
+ requestExport: (payload) =>
+ fetchExportPurchaseReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildPurchasePrintRows(records),
+ buildPreviewMeta: buildPreviewDialogMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildPurchaseReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || PURCHASE_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildPurchaseSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createPurchaseSearchState())
+ resetSearchParams()
+ }
+
+ onMounted(async () => {
+ await Promise.allSettled([loadTypeOptions(), loadWkTypeOptions(), loadExceStatusOptions()])
+ })
+</script>
diff --git a/rsf-design/src/views/orders/purchase/modules/purchase-detail-drawer.vue b/rsf-design/src/views/orders/purchase/modules/purchase-detail-drawer.vue
new file mode 100644
index 0000000..4018ab1
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase/modules/purchase-detail-drawer.vue
@@ -0,0 +1,91 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="PO鍗曡鎯�"
+ size="1180px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="PO鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉ユ簮鍗曞彿">{{ detail.source || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁绫诲瀷">{{ detail.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="闇�姹傛暟閲�">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸叉敹鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏀惰揣涓暟閲�">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏀惰揣閬撳彛">{{ detail.channel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ERP鍗曞彿">{{ detail.platCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰缂栫爜">{{ detail.project || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="棰勮鍒拌揪鏃堕棿">{{
+ detail.preArrText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁″垝鏀惰揣鏃堕棿">{{
+ detail.startTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁″垝缁撴潫鏃堕棿">{{
+ detail.endTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鐘舵��">{{
+ detail.exceStatusText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{
+ detail.createTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{
+ detail.updateTimeText || '--'
+ }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="space-y-3">
+ <div class="flex items-center justify-between">
+ <div class="text-sm font-medium text-[var(--art-gray-900)]">閲囪喘鏄庣粏</div>
+ <ElTag effect="plain">鍏� {{ itemRows.length }} 鏉�</ElTag>
+ </div>
+ <ArtTable :data="itemRows" :columns="itemColumns" :loading="itemsLoading" />
+ </div>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import ArtTable from '@/components/core/tables/art-table/index.vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ itemsLoading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ itemRows: { type: Array, default: () => [] },
+ itemColumns: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/orders/purchase/modules/purchase-dialog.vue b/rsf-design/src/views/orders/purchase/modules/purchase-dialog.vue
new file mode 100644
index 0000000..cdc833a
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase/modules/purchase-dialog.vue
@@ -0,0 +1,261 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildPurchaseDialogModel,
+ createPurchaseFormState,
+ getPurchaseStatusOptions
+ } from '../purchasePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ purchaseData: { type: Object, default: () => ({}) },
+ typeOptions: { type: Array, default: () => [] },
+ wkTypeOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createPurchaseFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫PO鍗�' : '鏂板PO鍗�'))
+
+ const rules = computed(() => ({
+ type: [{ required: true, message: '璇烽�夋嫨鍗曟嵁绫诲瀷', trigger: 'change' }],
+ wkType: [{ required: true, message: '璇烽�夋嫨涓氬姟绫诲瀷', trigger: 'change' }],
+ source: [{ required: true, message: '璇疯緭鍏ユ潵婧愬崟鍙�', trigger: 'blur' }],
+ anfme: [{ required: true, message: '璇疯緭鍏ラ渶姹傛暟閲�', trigger: 'blur' }]
+ }))
+
+ const dateProps = {
+ type: 'datetime',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ format: 'YYYY-MM-DD HH:mm:ss',
+ clearable: true
+ }
+
+ const formItems = computed(() => [
+ {
+ label: '鍗曟嵁绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鍗曟嵁绫诲瀷',
+ clearable: true,
+ filterable: true,
+ options: props.typeOptions
+ }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨涓氬姟绫诲瀷',
+ clearable: true,
+ filterable: true,
+ options: props.wkTypeOptions
+ }
+ },
+ {
+ label: '鏉ユ簮鍗曞彿',
+ key: 'source',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ潵婧愬崟鍙�',
+ clearable: true
+ }
+ },
+ {
+ label: '闇�姹傛暟閲�',
+ key: 'anfme',
+ type: 'input-number',
+ props: {
+ placeholder: '璇疯緭鍏ラ渶姹傛暟閲�',
+ min: 0,
+ precision: 2,
+ controlsPosition: 'right'
+ }
+ },
+ {
+ label: '棰勮鍒拌揪鏃堕棿',
+ key: 'preArr',
+ type: 'datetime',
+ props: {
+ ...dateProps,
+ placeholder: '璇烽�夋嫨棰勮鍒拌揪鏃堕棿'
+ }
+ },
+ {
+ label: '宸叉敹鏁伴噺',
+ key: 'qty',
+ type: 'input-number',
+ props: {
+ placeholder: '璇疯緭鍏ュ凡鏀舵暟閲�',
+ min: 0,
+ precision: 2,
+ controlsPosition: 'right'
+ }
+ },
+ {
+ label: '鏀惰揣涓暟閲�',
+ key: 'workQty',
+ type: 'input-number',
+ props: {
+ placeholder: '璇疯緭鍏ユ敹璐т腑鏁伴噺',
+ min: 0,
+ precision: 2,
+ controlsPosition: 'right'
+ }
+ },
+ {
+ label: '鏀惰揣閬撳彛',
+ key: 'channel',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ敹璐ч亾鍙�',
+ clearable: true
+ }
+ },
+ {
+ label: 'ERP鍗曞彿',
+ key: 'platCode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏RP鍗曞彿',
+ clearable: true
+ }
+ },
+ {
+ label: '璁″垝鏀惰揣鏃堕棿',
+ key: 'startTime',
+ type: 'datetime',
+ props: {
+ ...dateProps,
+ placeholder: '璇烽�夋嫨璁″垝鏀惰揣鏃堕棿'
+ }
+ },
+ {
+ label: '璁″垝缁撴潫鏃堕棿',
+ key: 'endTime',
+ type: 'datetime',
+ props: {
+ ...dateProps,
+ placeholder: '璇烽�夋嫨璁″垝缁撴潫鏃堕棿'
+ }
+ },
+ {
+ label: '椤圭洰缂栫爜',
+ key: 'project',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ」鐩紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: getPurchaseStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildPurchaseDialogModel(props.purchaseData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createPurchaseFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.purchaseData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/orders/purchase/purchasePage.helpers.js b/rsf-design/src/views/orders/purchase/purchasePage.helpers.js
new file mode 100644
index 0000000..fcce766
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase/purchasePage.helpers.js
@@ -0,0 +1,314 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const PURCHASE_REPORT_TITLE = 'PO鍗曟姤琛�'
+export const PURCHASE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeDateTimeText(value) {
+ return normalizeText(value)
+}
+
+export function createPurchaseSearchState() {
+ return {
+ condition: '',
+ code: '',
+ type: '',
+ wkType: '',
+ source: '',
+ channel: '',
+ platCode: '',
+ project: '',
+ exceStatus: '',
+ status: ''
+ }
+}
+
+export function createPurchaseFormState() {
+ return {
+ id: void 0,
+ code: '',
+ type: '',
+ wkType: '',
+ source: '',
+ preArr: '',
+ anfme: '',
+ qty: '',
+ workQty: '',
+ channel: '',
+ platCode: '',
+ startTime: '',
+ endTime: '',
+ project: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getPurchasePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getPurchaseStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getPurchaseStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildPurchaseSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ type: normalizeText(params.type),
+ wkType: normalizeText(params.wkType),
+ source: normalizeText(params.source),
+ channel: normalizeText(params.channel),
+ platCode: normalizeText(params.platCode),
+ project: normalizeText(params.project),
+ exceStatus:
+ params.exceStatus !== undefined && params.exceStatus !== null && params.exceStatus !== ''
+ ? normalizeNumber(params.exceStatus)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(
+ ([, value]) => value !== '' && value !== void 0 && value !== null
+ )
+ )
+}
+
+export function buildPurchasePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildPurchaseSearchParams(params)
+ }
+}
+
+export function buildPurchaseSavePayload(formData = {}) {
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: Number(formData.id) }
+ : {}),
+ code: normalizeText(formData.code) || '',
+ type: normalizeText(formData.type) || '',
+ wkType: normalizeText(formData.wkType) || '',
+ source: normalizeText(formData.source) || '',
+ ...(normalizeText(formData.preArr) ? { preArr: normalizeText(formData.preArr) } : {}),
+ ...(formData.anfme !== '' && formData.anfme !== null && formData.anfme !== undefined
+ ? { anfme: Number(formData.anfme) }
+ : {}),
+ ...(formData.qty !== '' && formData.qty !== null && formData.qty !== undefined
+ ? { qty: Number(formData.qty) }
+ : {}),
+ ...(formData.workQty !== '' && formData.workQty !== null && formData.workQty !== undefined
+ ? { workQty: Number(formData.workQty) }
+ : {}),
+ channel: normalizeText(formData.channel) || '',
+ platCode: normalizeText(formData.platCode) || '',
+ ...(normalizeText(formData.startTime) ? { startTime: normalizeText(formData.startTime) } : {}),
+ ...(normalizeText(formData.endTime) ? { endTime: normalizeText(formData.endTime) } : {}),
+ project: normalizeText(formData.project) || '',
+ status:
+ formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? Number(formData.status)
+ : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function buildPurchaseDialogModel(record = {}) {
+ return {
+ ...createPurchaseFormState(),
+ ...(record.id !== undefined && record.id !== null && record.id !== ''
+ ? { id: Number(record.id) }
+ : {}),
+ code: normalizeText(record.code || ''),
+ type: normalizeText(record.type || ''),
+ wkType: normalizeText(record.wkType || ''),
+ source: normalizeText(record.source || ''),
+ preArr: normalizeDateTimeText(record.preArr$ || record.preArr || ''),
+ anfme: record.anfme ?? '',
+ qty: record.qty ?? '',
+ workQty: record.workQty ?? '',
+ channel: normalizeText(record.channel || ''),
+ platCode: normalizeText(record.platCode || ''),
+ startTime: normalizeDateTimeText(record.startTime$ || record.startTime || ''),
+ endTime: normalizeDateTimeText(record.endTime$ || record.endTime || ''),
+ project: normalizeText(record.project || ''),
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function normalizePurchaseDetailRecord(record = {}) {
+ const statusMeta = getPurchaseStatusMeta(record.statusBool ?? record.status)
+ const exceStatusText = normalizeText(record.exceStatus$ || record.exceStatusText || '')
+ const typeText = normalizeText(record.type$ || record.typeText || record.type || '')
+ const wkTypeText = normalizeText(record.wkType$ || record.wkTypeText || record.wkType || '')
+
+ return {
+ ...record,
+ code: normalizeText(record.code) || '--',
+ typeText: typeText || '--',
+ wkTypeText: wkTypeText || '--',
+ source: normalizeText(record.source) || '--',
+ preArrText: normalizeDateTimeText(record.preArr$ || record.preArr) || '--',
+ anfme: record.anfme ?? '--',
+ qty: record.qty ?? '--',
+ workQty: record.workQty ?? '--',
+ channel: normalizeText(record.channel) || '--',
+ platCode: normalizeText(record.platCode) || '--',
+ startTimeText: normalizeDateTimeText(record.startTime$ || record.startTime) || '--',
+ endTimeText: normalizeDateTimeText(record.endTime$ || record.endTime) || '--',
+ project: normalizeText(record.project) || '--',
+ exceStatusText: exceStatusText || '--',
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo) || '--',
+ createByText: normalizeText(record.createBy$ || record.createByText || '') || '--',
+ createTimeText: normalizeDateTimeText(record.createTime$ || record.createTime) || '--',
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || '') || '--',
+ updateTimeText: normalizeDateTimeText(record.updateTime$ || record.updateTime) || '--'
+ }
+}
+
+export function normalizePurchaseListRow(record = {}) {
+ return normalizePurchaseDetailRecord(record)
+}
+
+export function normalizePurchaseItemRow(record = {}) {
+ const statusMeta = getPurchaseStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ purchaseId: record.purchaseId ?? '--',
+ platItemId: normalizeText(record.platItemId) || '--',
+ matnrCode: normalizeText(record.matnrCode) || '--',
+ matnrName: normalizeText(record.matnrName) || '--',
+ unit: normalizeText(record.unit) || '--',
+ anfme: record.anfme ?? '--',
+ qty: record.qty ?? '--',
+ nromQty: record.nromQty ?? '--',
+ asnQty: record.asnQty ?? '--',
+ printQty: record.printQty ?? '--',
+ splrName: normalizeText(record.splrName) || '--',
+ splrCode: normalizeText(record.splrCode) || '--',
+ splrBatch: normalizeText(record.splrBatch) || '--',
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function buildPurchasePrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizePurchaseListRow(record))
+}
+
+export function buildPurchaseReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = PURCHASE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: PURCHASE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...PURCHASE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function resolveDictOptions(records = [], options = {}) {
+ const { group } = options
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records
+ .filter((item) => {
+ if (!item || typeof item !== 'object') {
+ return false
+ }
+ if (group === undefined) {
+ return true
+ }
+ return normalizeText(item.group) === normalizeText(group)
+ })
+ .map((item) => {
+ const value = item.value ?? item.id ?? item.dictValue
+ if (value === undefined || value === null || value === '') {
+ return null
+ }
+ return {
+ value: normalizeText(value),
+ label: normalizeText(item.label || item.name || item.dictLabel || value)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function createPurchaseItemColumns() {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 70, align: 'center' },
+ { prop: 'platItemId', label: 'ERP琛屽彿', minWidth: 110, showOverflowTooltip: true },
+ { prop: 'matnrCode', label: '鐗╂枡缂栫爜', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'matnrName', label: '鐗╂枡鍚嶇О', minWidth: 180, showOverflowTooltip: true },
+ { prop: 'unit', label: '鍗曚綅', width: 90, align: 'center' },
+ { prop: 'anfme', label: '鏁伴噺', width: 100, align: 'right' },
+ { prop: 'qty', label: '宸叉敹鏁伴噺', width: 100, align: 'right' },
+ { prop: 'nromQty', label: '鏍囧噯鍖呰', width: 100, align: 'right' },
+ { prop: 'asnQty', label: 'ASN鏁伴噺', width: 100, align: 'right' },
+ { prop: 'printQty', label: '鎵撳嵃鏁伴噺', width: 100, align: 'right' },
+ { prop: 'splrName', label: '渚涘簲鍟嗗悕绉�', minWidth: 150, showOverflowTooltip: true },
+ { prop: 'splrCode', label: '渚涘簲鍟嗙紪鐮�', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'splrBatch', label: '渚涘簲鍟嗘壒娆�', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'statusText', label: '鐘舵��', width: 90, align: 'center' },
+ { prop: 'memo', label: '澶囨敞', minWidth: 160, showOverflowTooltip: true }
+ ]
+}
diff --git a/rsf-design/src/views/orders/purchase/purchaseTable.columns.js b/rsf-design/src/views/orders/purchase/purchaseTable.columns.js
new file mode 100644
index 0000000..03a3bbf
--- /dev/null
+++ b/rsf-design/src/views/orders/purchase/purchaseTable.columns.js
@@ -0,0 +1,179 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getPurchaseStatusMeta } from './purchasePage.helpers'
+
+export function createPurchaseTableColumns({
+ handleView,
+ handleEdit,
+ handleDelete,
+ canEdit = true,
+ canDelete = true,
+ canDeleteRow = () => true
+} = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: 'PO鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'typeText',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeText || '--'
+ },
+ {
+ prop: 'wkTypeText',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.wkTypeText || '--'
+ },
+ {
+ prop: 'source',
+ label: '鏉ユ簮鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.source || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '闇�姹傛暟閲�',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鏀惰揣涓暟閲�',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'channel',
+ label: '鏀惰揣閬撳彛',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.channel || '--'
+ },
+ {
+ prop: 'platCode',
+ label: 'ERP鍗曞彿',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platCode || '--'
+ },
+ {
+ prop: 'preArrText',
+ label: '棰勮鍒拌揪鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.preArrText || '--'
+ },
+ {
+ prop: 'startTimeText',
+ label: '璁″垝鏀惰揣鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.startTimeText || '--'
+ },
+ {
+ prop: 'endTimeText',
+ label: '璁″垝缁撴潫鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.endTimeText || '--'
+ },
+ {
+ prop: 'project',
+ label: '椤圭洰缂栫爜',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.project || '--'
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鎵ц鐘舵��',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.exceStatusText || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getPurchaseStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 160,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) => {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (canEdit && handleEdit) {
+ operations.push({ key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' })
+ }
+
+ if (canDelete && handleDelete && canDeleteRow(row)) {
+ operations.push({
+ key: 'delete',
+ label: '鍒犻櫎',
+ icon: 'ri:delete-bin-5-line',
+ color: 'var(--art-error)'
+ })
+ }
+
+ return h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'edit') handleEdit?.(row)
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/transfer-item/index.vue b/rsf-design/src/views/orders/transfer-item/index.vue
new file mode 100644
index 0000000..47bff6c
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer-item/index.vue
@@ -0,0 +1,429 @@
+<template>
+ <div class="transfer-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <TransferItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import {
+ fetchExportTransferItemReport,
+ fetchTransferItemDetail,
+ fetchTransferItemMany,
+ fetchTransferItemPage
+ } from '@/api/transfer-item'
+ import TransferItemDetailDrawer from './modules/transfer-item-detail-drawer.vue'
+ import { createTransferItemTableColumns } from './transferItemTable.columns.js'
+ import {
+ TRANSFER_ITEM_REPORT_STYLE,
+ TRANSFER_ITEM_REPORT_TITLE,
+ buildTransferItemPageQueryParams,
+ buildTransferItemPrintRows,
+ buildTransferItemReportMeta,
+ buildTransferItemSearchParams,
+ createTransferItemSearchState,
+ getTransferItemPaginationKey,
+ getTransferItemStatusOptions,
+ normalizeTransferItemRow
+ } from './transferItemPage.helpers.js'
+
+ defineOptions({ name: 'TransferItem' })
+
+ const userStore = useUserStore()
+ const reportTitle = TRANSFER_ITEM_REPORT_TITLE
+ const searchForm = ref(createTransferItemSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ皟鎷ㄥ崟鍙�/鐗╂枡缂栫爜/鐗╂枡鍚嶇О/澶囨敞'
+ }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'datetime',
+ props: {
+ clearable: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨寮�濮嬫椂闂�'
+ }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'datetime',
+ props: {
+ clearable: true,
+ format: 'YYYY-MM-DD HH:mm:ss',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss',
+ placeholder: '璇烽�夋嫨缁撴潫鏃堕棿'
+ }
+ },
+ {
+ label: '璋冩嫧鍗旾D',
+ key: 'transferId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ皟鎷ㄥ崟ID'
+ }
+ },
+ {
+ label: '璋冩嫧鍗曞彿',
+ key: 'transferCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ皟鎷ㄥ崟鍙�'
+ }
+ },
+ {
+ label: '鐗╂枡ID',
+ key: 'matnrId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ墿鏂橧D'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鍗曚綅',
+ key: 'unit',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ崟浣�'
+ }
+ },
+ {
+ label: '鏁伴噺',
+ key: 'anfme',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ暟閲�'
+ }
+ },
+ {
+ label: '宸插畬鎴愭暟閲�',
+ key: 'qty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ュ凡瀹屾垚鏁伴噺'
+ }
+ },
+ {
+ label: '鎵ц鏁伴噺',
+ key: 'workQty',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ墽琛屾暟閲�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ },
+ {
+ label: '渚涘簲鍟咺D',
+ key: 'splrId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢ID'
+ }
+ },
+ {
+ label: '瑙勬牸',
+ key: 'spec',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鏍�'
+ }
+ },
+ {
+ label: '鍨嬪彿',
+ key: 'model',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瀷鍙�'
+ }
+ },
+ {
+ label: '瀛楁绱㈠紩',
+ key: 'fieldsIndex',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈电储寮�'
+ }
+ },
+ {
+ label: '骞冲彴琛屽彿',
+ key: 'platItemId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ钩鍙拌鍙�'
+ }
+ },
+ {
+ label: '瀹㈡埛璁㈠崟鍙�',
+ key: 'platOrderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鎴疯鍗曞彿'
+ }
+ },
+ {
+ label: '宸ュ崟鍙�',
+ key: 'platWorkCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ伐鍗曞彿'
+ }
+ },
+ {
+ label: '椤圭洰鍙�',
+ key: 'projectCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ」鐩彿'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getTransferItemStatusOptions()
+ }
+ }
+ ])
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ const reportQueryParams = computed(() => buildTransferItemSearchParams(searchForm.value))
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchTransferItemPage,
+ apiParams: buildTransferItemPageQueryParams(searchForm.value),
+ paginationKey: getTransferItemPaginationKey(),
+ columnsFactory: () =>
+ createTransferItemTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeTransferItemRow(item)) : []
+ }
+ })
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchTransferItemDetail(row.id), {}, {
+ timeoutMessage: '璋冩嫧鏄庣粏璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeTransferItemRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇璋冩嫧鏄庣粏璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildTransferItemPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTransferItemSearchState())
+ resetSearchParams()
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchTransferItemMany(payload.ids)).records
+ }
+
+ return defaultResponseAdapter(
+ await fetchTransferItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'transfer-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportTransferItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTransferItemPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...TRANSFER_ITEM_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildTransferItemReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || TRANSFER_ITEM_REPORT_STYLE.orientation
+ })
+ )
+</script>
+
diff --git a/rsf-design/src/views/orders/transfer-item/modules/transfer-item-detail-drawer.vue b/rsf-design/src/views/orders/transfer-item/modules/transfer-item-detail-drawer.vue
new file mode 100644
index 0000000..4a824f6
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer-item/modules/transfer-item-detail-drawer.vue
@@ -0,0 +1,78 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="璋冩嫧鏄庣粏璇︽儏"
+ size="72%"
+ destroy-on-close
+ append-to-body
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions title="鍩虹淇℃伅" :column="3" border>
+ <ElDescriptionsItem label="璋冩嫧鍗旾D">{{ displayData.transferId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璋冩嫧鍗曞彿">{{ displayData.transferCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType || 'info'" effect="light">
+ {{ displayData.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗙紪鐮�">{{ displayData.splrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟咺D">{{ displayData.splrId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗗悕绉�">{{ displayData.splrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="3">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="鐗╂枡淇℃伅" :column="3" border>
+ <ElDescriptionsItem label="鐗╂枡ID">{{ displayData.matnrId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ displayData.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ displayData.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勬牸">{{ displayData.spec || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍨嬪彿">{{ displayData.model || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ displayData.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ displayData.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ displayData.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ displayData.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸插畬鎴愭暟閲�">{{ displayData.qty ?? '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="骞冲彴淇℃伅" :column="3" border>
+ <ElDescriptionsItem label="瀛楁绱㈠紩">{{ displayData.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴琛屽彿">{{ displayData.platItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛璁㈠崟鍙�">{{ displayData.platOrderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ュ崟鍙�">{{ displayData.platWorkCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰鍙�">{{ displayData.projectCode || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ displayData.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElSkeleton>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import { normalizeTransferItemRow } from '../transferItemPage.helpers.js'
+
+ defineOptions({ name: 'TransferItemDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeTransferItemRow(props.detail))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/transfer-item/transferItemPage.helpers.js b/rsf-design/src/views/orders/transfer-item/transferItemPage.helpers.js
new file mode 100644
index 0000000..bce45e0
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer-item/transferItemPage.helpers.js
@@ -0,0 +1,205 @@
+export const TRANSFER_ITEM_REPORT_TITLE = '璋冩嫧鏄庣粏鎶ヨ〃'
+export const TRANSFER_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true,
+ showBorder: true
+}
+
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+function normalizeText(value, fallback = '--') {
+ if (value === null || value === undefined || value === '') {
+ return fallback
+ }
+ const text = String(value).trim()
+ return text || fallback
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function pushText(target, key, value) {
+ const text = normalizeText(value, '')
+ if (text) {
+ target[key] = text
+ }
+}
+
+function pushNumber(target, key, value) {
+ const numericValue = normalizeNumber(value, void 0)
+ if (numericValue !== void 0) {
+ target[key] = numericValue
+ }
+}
+
+function getStatusMeta(status, statusText) {
+ const numericStatus = Number(status)
+ return STATUS_META[numericStatus] || {
+ text: normalizeText(statusText || status || '--'),
+ type: 'info',
+ bool: false
+ }
+}
+
+export function createTransferItemSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ transferId: '',
+ transferCode: '',
+ matnrId: '',
+ maktx: '',
+ matnrCode: '',
+ unit: '',
+ anfme: '',
+ qty: '',
+ workQty: '',
+ batch: '',
+ splrId: '',
+ spec: '',
+ model: '',
+ fieldsIndex: '',
+ platItemId: '',
+ platOrderCode: '',
+ platWorkCode: '',
+ projectCode: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getTransferItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getTransferItemStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getTransferItemStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildTransferItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'timeStart',
+ 'timeEnd',
+ 'transferCode',
+ 'maktx',
+ 'matnrCode',
+ 'unit',
+ 'batch',
+ 'spec',
+ 'model',
+ 'fieldsIndex',
+ 'platItemId',
+ 'platOrderCode',
+ 'platWorkCode',
+ 'projectCode',
+ 'memo'
+ ].forEach((key) => pushText(result, key, params[key]))
+
+ ;['transferId', 'matnrId', 'anfme', 'qty', 'workQty', 'splrId', 'status'].forEach((key) =>
+ pushNumber(result, key, params[key])
+ )
+
+ return result
+}
+
+export function buildTransferItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTransferItemSearchParams(params)
+ }
+}
+
+export function normalizeTransferItemRow(record = {}) {
+ const statusMeta = getStatusMeta(record.statusBool ?? record.status, record['status$'] || record.statusText)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ transferId: normalizeNumber(record.transferId, '--'),
+ transferCode: normalizeText(record.transferCode),
+ matnrId: normalizeNumber(record.matnrId, '--'),
+ maktx: normalizeText(record.maktx),
+ matnrCode: normalizeText(record.matnrCode),
+ unit: normalizeText(record.unit),
+ anfme: normalizeNumber(record.anfme, '--'),
+ qty: normalizeNumber(record.qty, '--'),
+ workQty: normalizeNumber(record.workQty, '--'),
+ batch: normalizeText(record.batch),
+ splrId: normalizeNumber(record.splrId, '--'),
+ splrCode: normalizeText(record.splrCode),
+ splrName: normalizeText(record.splrName),
+ spec: normalizeText(record.spec),
+ model: normalizeText(record.model),
+ fieldsIndex: normalizeText(record.fieldsIndex),
+ platItemId: normalizeText(record.platItemId),
+ platOrderCode: normalizeText(record.platOrderCode),
+ platWorkCode: normalizeText(record.platWorkCode),
+ projectCode: normalizeText(record.projectCode),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record['createBy$'] || record.createByText),
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime, '--'),
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText),
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime, '--'),
+ memo: normalizeText(record.memo)
+ }
+}
+
+export function buildTransferItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeTransferItemRow(record))
+}
+
+export function buildTransferItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = TRANSFER_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: TRANSFER_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...TRANSFER_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
diff --git a/rsf-design/src/views/orders/transfer-item/transferItemTable.columns.js b/rsf-design/src/views/orders/transfer-item/transferItemTable.columns.js
new file mode 100644
index 0000000..038fdd8
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer-item/transferItemTable.columns.js
@@ -0,0 +1,164 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createTransferItemTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'transferId',
+ label: '璋冩嫧鍗旾D',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.transferId ?? '--'
+ },
+ {
+ prop: 'transferCode',
+ label: '璋冩嫧鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.transferCode || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'spec',
+ label: '瑙勬牸',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.spec || '--'
+ },
+ {
+ prop: 'model',
+ label: '鍨嬪彿',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.model || '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸插畬鎴愭暟閲�',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'splrCode',
+ label: '渚涘簲鍟嗙紪鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrCode || '--'
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟嗗悕绉�',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrName || '--'
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '瀛楁绱㈠紩',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.fieldsIndex || '--'
+ },
+ {
+ prop: 'platItemId',
+ label: '骞冲彴琛屽彿',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platItemId || '--'
+ },
+ {
+ prop: 'platOrderCode',
+ label: '瀹㈡埛璁㈠崟鍙�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platOrderCode || '--'
+ },
+ {
+ prop: 'platWorkCode',
+ label: '宸ュ崟鍙�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.platWorkCode || '--'
+ },
+ {
+ prop: 'projectCode',
+ label: '椤圭洰鍙�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.projectCode || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '--')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 96,
+ fixed: 'right',
+ align: 'center',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
+
diff --git a/rsf-design/src/views/orders/transfer/index.vue b/rsf-design/src/views/orders/transfer/index.vue
new file mode 100644
index 0000000..f85b2d4
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer/index.vue
@@ -0,0 +1,474 @@
+<template>
+ <div class="transfer-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-if="canCreate" type="primary" @click="showDialog('add')" v-ripple>鏂板璋冩嫧鍗�</ElButton>
+ <ElButton
+ v-if="canDelete"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <TransferDialog
+ v-model:visible="dialogVisible"
+ :dialog-type="dialogType"
+ :transfer-data="currentTransferData"
+ :type-options="typeOptions"
+ :area-options="areaOptions"
+ :submit-loading="dialogSubmitting"
+ @submit="handleDialogSubmit"
+ />
+
+ <TransferDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :orders-loading="detailOrdersLoading"
+ :detail="detailData"
+ :order-rows="detailOrderRows"
+ :order-pagination="detailOrderPagination"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
+ import {
+ fetchDeleteTransfer,
+ fetchExportTransferReport,
+ fetchTransferDetail,
+ fetchTransferMany,
+ fetchTransferOrdersPage,
+ fetchTransferPage,
+ fetchTransferPubOutStock,
+ fetchSaveTransfer,
+ fetchUpdateTransfer
+ } from '@/api/transfer'
+ import TransferDialog from './modules/transfer-dialog.vue'
+ import TransferDetailDrawer from './modules/transfer-detail-drawer.vue'
+ import { createTransferTableColumns } from './transferTable.columns.js'
+ import {
+ TRANSFER_REPORT_STYLE,
+ TRANSFER_REPORT_TITLE,
+ buildTransferDetailOrderQueryParams,
+ buildTransferDialogModel,
+ buildTransferPageQueryParams,
+ buildTransferPrintRows,
+ buildTransferReportMeta,
+ buildTransferSavePayload,
+ buildTransferSearchParams,
+ createTransferFormState,
+ createTransferSearchState,
+ getTransferPaginationKey,
+ getTransferSourceOptions,
+ getTransferStatusOptions,
+ getTransferExceStatusOptions,
+ normalizeTransferDetailRecord,
+ normalizeTransferOrderRow,
+ normalizeTransferRow,
+ resolveTransferAreaOptions,
+ resolveTransferTypeOptions
+ } from './transferPage.helpers.js'
+
+ defineOptions({ name: 'Transfer' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const reportTitle = TRANSFER_REPORT_TITLE
+ const searchForm = ref(createTransferSearchState())
+ const typeOptions = ref([])
+ const areaOptions = ref([])
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailOrdersLoading = ref(false)
+ const detailData = ref({})
+ const detailOrderRows = ref([])
+ const activeTransferId = ref(null)
+ const activeTransferCode = ref('')
+ const dialogSubmitting = ref(false)
+ const detailOrderPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const canCreate = computed(() => hasAuth('add'))
+ const canDelete = computed(() => hasAuth('delete'))
+ const canUpdate = computed(() => hasAuth('update'))
+
+ const reportQueryParams = computed(() => buildTransferSearchParams(searchForm.value))
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ崟鍙�/澶囨敞/浠撳簱/搴撳尯' } },
+ { label: '璋冩嫧鍗曞彿', key: 'code', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ皟鎷ㄥ崟鍙�' } },
+ {
+ label: '璋冩嫧绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: { clearable: true, filterable: true, options: typeOptions.value }
+ },
+ {
+ label: '鏉ユ簮',
+ key: 'source',
+ type: 'select',
+ props: { clearable: true, options: getTransferSourceOptions() }
+ },
+ {
+ label: '鎵ц鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: { clearable: true, options: getTransferExceStatusOptions() }
+ },
+ { label: '婧愪粨搴�', key: 'orgWareName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ簮浠撳簱' } },
+ { label: '鐩爣浠撳簱', key: 'tarWareName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洰鏍囦粨搴�' } },
+ { label: '婧愬簱鍖�', key: 'orgAreaName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ簮搴撳尯' } },
+ { label: '鐩爣搴撳尯', key: 'tarAreaName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ洰鏍囧簱鍖�' } },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: { clearable: true, options: getTransferStatusOptions() }
+ },
+ { label: '澶囨敞', key: 'memo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' } }
+ ])
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ handleCrudSelectionChange(rows)
+ }
+
+ async function loadTransferDetail(transferId) {
+ detailLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchTransferDetail(transferId),
+ {},
+ { timeoutMessage: '璋冩嫧鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ detailData.value = normalizeTransferDetailRecord(response)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function loadTransferOrders(code) {
+ detailOrdersLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchTransferOrdersPage(
+ buildTransferDetailOrderQueryParams({
+ code,
+ current: detailOrderPagination.current,
+ pageSize: detailOrderPagination.size
+ })
+ ),
+ { records: [], total: 0, current: detailOrderPagination.current, size: detailOrderPagination.size },
+ { timeoutMessage: '鍏宠仈鍗曟嵁鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ const normalized = defaultResponseAdapter(response)
+ detailOrderRows.value = normalized.records.map((item) => normalizeTransferOrderRow(item))
+ detailOrderPagination.total = Number(normalized.total || 0)
+ detailOrderPagination.current = Number(normalized.current || detailOrderPagination.current || 1)
+ detailOrderPagination.size = Number(normalized.size || detailOrderPagination.size || 20)
+ } catch (error) {
+ detailOrderRows.value = []
+ detailOrderPagination.total = 0
+ ElMessage.error(error?.message || '鑾峰彇鍏宠仈鍗曟嵁澶辫触')
+ } finally {
+ detailOrdersLoading.value = false
+ }
+ }
+
+ async function openDetail(row) {
+ activeTransferId.value = row.id
+ activeTransferCode.value = row.code || ''
+ detailOrderPagination.current = 1
+ detailOrderRows.value = []
+ detailData.value = {}
+ detailDrawerVisible.value = true
+ try {
+ await loadTransferDetail(row.id)
+ activeTransferCode.value = detailData.value.code || row.code || activeTransferCode.value
+ await loadTransferOrders(activeTransferCode.value)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ detailOrderRows.value = []
+ ElMessage.error(error?.message || '鑾峰彇璋冩嫧鍗曡鎯呭け璐�')
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(
+ fetchTransferDetail(row.id),
+ {},
+ { timeoutMessage: '璋冩嫧鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ showDialog('edit', detail)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇璋冩嫧鍗曡鎯呭け璐�')
+ }
+ }
+
+ async function handlePublish(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佷笅鍙戣皟鎷ㄥ崟銆�${row.code || row.id}銆嶅悧锛焋, '涓嬪彂纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ const response = await fetchTransferPubOutStock({ id: row.id })
+ if (response?.code !== 200 && response?.success !== true) {
+ throw new Error(response?.message || '涓嬪彂鎵ц澶辫触')
+ }
+ ElMessage.success(response?.message || '涓嬪彂鎵ц鎴愬姛')
+ await refreshData()
+ if (detailDrawerVisible.value && activeTransferId.value === row.id) {
+ await loadTransferDetail(row.id)
+ await loadTransferOrders(row.code || activeTransferCode.value)
+ }
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '涓嬪彂鎵ц澶辫触')
+ }
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) return
+ if (action?.key === 'view') {
+ await openDetail(row)
+ return
+ }
+ if (action?.key === 'edit') {
+ if (!canUpdate.value) return
+ await openEditDialog(row)
+ return
+ }
+ if (action?.key === 'delete') {
+ if (!canDelete.value) return
+ await handleDeleteAction?.(row)
+ return
+ }
+ if (action?.key === 'publish') {
+ if (!canUpdate.value) return
+ await handlePublish(row)
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchTransferPage,
+ apiParams: buildTransferPageQueryParams(searchForm.value),
+ paginationKey: getTransferPaginationKey(),
+ columnsFactory: () => createTransferTableColumns({ handleActionClick })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeTransferRow(item)) : [])
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentTransferData,
+ selectedRows: crudSelectedRows,
+ handleSelectionChange: handleCrudSelectionChange,
+ showDialog,
+ handleDialogSubmit: handleCrudDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => createTransferFormState(),
+ buildEditModel: (record) => buildTransferDialogModel(record),
+ buildSavePayload: (formData) => buildTransferSavePayload(formData, areaOptions.value),
+ saveRequest: fetchSaveTransfer,
+ updateRequest: fetchUpdateTransfer,
+ deleteRequest: fetchDeleteTransfer,
+ entityName: '璋冩嫧鍗�',
+ resolveRecordLabel: (record) => record?.code || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ const handleDeleteAction = handleDelete
+
+ function handleDialogSubmit(formData) {
+ dialogSubmitting.value = true
+ return handleCrudDialogSubmit(formData).finally(() => {
+ dialogSubmitting.value = false
+ })
+ }
+
+ async function loadTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({ current: 1, pageSize: 200, dictTypeCode: 'sys_transfer_type', status: 1 }),
+ { records: [] },
+ { timeoutMessage: '璋冩嫧绫诲瀷閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ typeOptions.value = resolveTransferTypeOptions(defaultResponseAdapter(response).records)
+ }
+
+ async function loadAreaOptions() {
+ const response = await guardRequestWithMessage(
+ fetchWarehouseAreasList(),
+ { records: [] },
+ { timeoutMessage: '搴撳尯閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ areaOptions.value = resolveTransferAreaOptions(defaultResponseAdapter(response).records)
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildTransferSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createTransferSearchState()
+ resetSearchParams()
+ }
+
+ async function handleDetailCurrentChange(current) {
+ detailOrderPagination.current = current
+ await loadTransferOrders(activeTransferCode.value)
+ }
+
+ async function handleDetailSizeChange(size) {
+ detailOrderPagination.size = size
+ detailOrderPagination.current = 1
+ await loadTransferOrders(activeTransferCode.value)
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchTransferMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchTransferPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta: rawPreviewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'transfer.xlsx',
+ requestExport: (payload) =>
+ fetchExportTransferReport(Array.isArray(payload?.ids) && payload.ids.length > 0 ? reportQueryParams.value : payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTransferPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildTransferReportMeta({
+ previewMeta: rawPreviewMeta.value,
+ count: previewRows.value.length,
+ orientation: rawPreviewMeta.value?.reportStyle?.orientation || TRANSFER_REPORT_STYLE.orientation
+ })
+ )
+
+ onMounted(async () => {
+ await Promise.allSettled([loadTypeOptions(), loadAreaOptions(), getData()])
+ })
+</script>
diff --git a/rsf-design/src/views/orders/transfer/modules/transfer-detail-drawer.vue b/rsf-design/src/views/orders/transfer/modules/transfer-detail-drawer.vue
new file mode 100644
index 0000000..21fe181
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer/modules/transfer-detail-drawer.vue
@@ -0,0 +1,93 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="璋冩嫧鍗曡鎯�"
+ size="1200px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="璋冩嫧鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璋冩嫧绫诲瀷">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉ユ簮">
+ <ElTag :type="detail.sourceTagType || 'info'" effect="light">
+ {{ detail.sourceText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鐘舵��">
+ <ElTag :type="detail.exceStatusTagType || 'info'" effect="light">
+ {{ detail.exceStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愪粨搴�">{{ detail.orgWareName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣浠撳簱">{{ detail.tarWareName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="婧愬簱鍖�">{{ detail.orgAreaName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣搴撳尯">{{ detail.tarAreaName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="space-y-3">
+ <div class="flex items-center justify-between">
+ <div class="text-sm font-medium text-[var(--art-gray-900)]">鍏宠仈鍗曟嵁</div>
+ <ElTag effect="plain">鍏� {{ orderRows.length }} 鏉�</ElTag>
+ </div>
+ <ArtTable
+ :loading="ordersLoading"
+ :data="orderRows"
+ :columns="orderColumns"
+ :pagination="orderPagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import ArtTable from '@/components/core/tables/art-table/index.vue'
+ import { createTransferOrderTableColumns } from '../transferTable.columns.js'
+
+ defineOptions({ name: 'TransferDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ ordersLoading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ orderRows: { type: Array, default: () => [] },
+ orderPagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'size-change', 'current-change'])
+
+ const orderColumns = createTransferOrderTableColumns()
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/orders/transfer/modules/transfer-dialog.vue b/rsf-design/src/views/orders/transfer/modules/transfer-dialog.vue
new file mode 100644
index 0000000..9b14805
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer/modules/transfer-dialog.vue
@@ -0,0 +1,184 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="760px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <div class="mb-3 rounded-lg border border-[var(--art-border-color)] bg-[var(--art-bg-color)] px-3 py-2 text-xs text-[var(--art-text-gray-600)]">
+ 璋冩嫧鍗曞彿鐢辩郴缁熺敓鎴愶紝鏂板鏃跺彧闇�缁存姢璋冩嫧绫诲瀷銆佹簮/鐩爣搴撳尯鍜屽娉ㄣ��
+ </div>
+
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" :loading="submitLoading" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildTransferDialogModel,
+ createTransferFormState,
+ getTransferStatusOptions
+ } from '../transferPage.helpers.js'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dialogType: { type: String, default: 'add' },
+ transferData: { type: Object, default: () => ({}) },
+ typeOptions: { type: Array, default: () => [] },
+ areaOptions: { type: Array, default: () => [] },
+ submitLoading: { type: Boolean, default: false }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createTransferFormState())
+
+ const isEdit = computed(() => props.dialogType === 'edit')
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫璋冩嫧鍗�' : '鏂板璋冩嫧鍗�'))
+
+ const rules = computed(() => ({
+ type: [{ required: true, message: '璇烽�夋嫨璋冩嫧绫诲瀷', trigger: 'change' }],
+ orgAreaId: [{ required: true, message: '璇烽�夋嫨婧愬簱鍖�', trigger: 'change' }],
+ tarAreaId: [{ required: true, message: '璇烽�夋嫨鐩爣搴撳尯', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '璋冩嫧鍗曞彿',
+ key: 'code',
+ type: 'input',
+ span: 24,
+ props: {
+ disabled: true,
+ placeholder: '淇濆瓨鍚庤嚜鍔ㄧ敓鎴�'
+ }
+ },
+ {
+ label: '璋冩嫧绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨璋冩嫧绫诲瀷',
+ clearable: true,
+ filterable: true,
+ options: props.typeOptions
+ }
+ },
+ {
+ label: '婧愬簱鍖�',
+ key: 'orgAreaId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨婧愬簱鍖�',
+ clearable: true,
+ filterable: true,
+ options: props.areaOptions
+ }
+ },
+ {
+ label: '鐩爣搴撳尯',
+ key: 'tarAreaId',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐩爣搴撳尯',
+ clearable: true,
+ filterable: true,
+ options: props.areaOptions
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: getTransferStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const loadFormData = () => {
+ Object.assign(form, buildTransferDialogModel(props.transferData))
+ }
+
+ const resetForm = () => {
+ Object.assign(form, createTransferFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.transferData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/orders/transfer/transferPage.helpers.js b/rsf-design/src/views/orders/transfer/transferPage.helpers.js
new file mode 100644
index 0000000..7e06d2c
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer/transferPage.helpers.js
@@ -0,0 +1,347 @@
+const TRANSFER_SOURCE_META = {
+ 1: { text: 'ERP绯荤粺', type: 'info' },
+ 2: { text: 'WMS绯荤粺鐢熸垚', type: 'primary' },
+ 3: { text: 'EXCEL瀵煎叆', type: 'warning' },
+ 4: { text: 'QMS绯荤粺', type: 'success' }
+}
+
+const TRANSFER_EXCE_STATUS_META = {
+ 0: { text: '鏈墽琛�', type: 'info' },
+ 1: { text: '鎵ц涓�', type: 'warning' },
+ 2: { text: '鎵ц瀹屾垚', type: 'success' }
+}
+
+const TRANSFER_STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export const TRANSFER_REPORT_TITLE = '璋冩嫧鍗曟姤琛�'
+export const TRANSFER_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) return fallback
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function metaByValue(value, metaMap, fallbackText = '--') {
+ const numericValue = Number(value)
+ return metaMap[numericValue] || { text: normalizeText(value) || fallbackText, type: 'info' }
+}
+
+export function createTransferSearchState() {
+ return {
+ condition: '',
+ code: '',
+ type: '',
+ source: '',
+ exceStatus: '',
+ orgWareName: '',
+ tarWareName: '',
+ orgAreaName: '',
+ tarAreaName: '',
+ memo: '',
+ status: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createTransferFormState() {
+ return {
+ id: void 0,
+ code: '',
+ type: '',
+ source: 2,
+ exceStatus: 0,
+ orgAreaId: void 0,
+ tarAreaId: void 0,
+ status: 1,
+ memo: ''
+ }
+}
+
+export function buildTransferDialogModel(record = {}) {
+ return {
+ ...createTransferFormState(),
+ ...(record.id !== undefined && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
+ code: normalizeText(record.code || ''),
+ type: record.type !== undefined && record.type !== null && record.type !== '' ? Number(record.type) : '',
+ source: record.source !== undefined && record.source !== null && record.source !== '' ? Number(record.source) : 2,
+ exceStatus:
+ record.exceStatus !== undefined && record.exceStatus !== null && record.exceStatus !== ''
+ ? Number(record.exceStatus)
+ : 0,
+ orgAreaId:
+ record.orgAreaId !== undefined && record.orgAreaId !== null && record.orgAreaId !== ''
+ ? Number(record.orgAreaId)
+ : void 0,
+ tarAreaId:
+ record.tarAreaId !== undefined && record.tarAreaId !== null && record.tarAreaId !== ''
+ ? Number(record.tarAreaId)
+ : void 0,
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function getTransferPaginationKey() {
+ return { current: 'current', size: 'pageSize' }
+}
+
+export function getTransferSourceOptions() {
+ return [
+ { label: 'ERP绯荤粺', value: 1 },
+ { label: 'WMS绯荤粺鐢熸垚', value: 2 },
+ { label: 'EXCEL瀵煎叆', value: 3 },
+ { label: 'QMS绯荤粺', value: 4 }
+ ]
+}
+
+export function getTransferStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getTransferExceStatusOptions() {
+ return [
+ { label: '鏈墽琛�', value: 0 },
+ { label: '鎵ц涓�', value: 1 },
+ { label: '鎵ц瀹屾垚', value: 2 }
+ ]
+}
+
+export function buildTransferSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'orgWareName', 'tarWareName', 'orgAreaName', 'tarAreaName', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) result[key] = value
+ })
+ ;['type', 'source', 'exceStatus', 'status', 'orgWareId', 'tarWareId', 'orgAreaId', 'tarAreaId'].forEach((key) => {
+ if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
+ result[key] = normalizeNumber(params[key])
+ }
+ })
+ if (params.timeStart !== '' && params.timeStart !== undefined && params.timeStart !== null) {
+ result.timeStart = normalizeText(params.timeStart)
+ }
+ if (params.timeEnd !== '' && params.timeEnd !== undefined && params.timeEnd !== null) {
+ result.timeEnd = normalizeText(params.timeEnd)
+ }
+ return result
+}
+
+export function buildTransferPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTransferSearchParams(params)
+ }
+}
+
+export function buildTransferDetailOrderQueryParams(params = {}) {
+ return {
+ condition: normalizeText(params.code || params.condition),
+ code: normalizeText(params.code || params.condition),
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function buildTransferSavePayload(formData = {}, areaOptions = []) {
+ const optionMap = new Map(
+ (Array.isArray(areaOptions) ? areaOptions : [])
+ .map((item) => {
+ const value = normalizeNumber(item?.value ?? item?.id, void 0)
+ if (value === void 0) return null
+ return [value, item?.raw || item]
+ })
+ .filter(Boolean)
+ )
+
+ const orgAreaId = normalizeNumber(formData.orgAreaId, void 0)
+ const tarAreaId = normalizeNumber(formData.tarAreaId, void 0)
+ const orgArea = optionMap.get(orgAreaId) || {}
+ const tarArea = optionMap.get(tarAreaId) || {}
+ const orgWareId = normalizeNumber(orgArea.warehouseId ?? orgArea.warehouse_id ?? orgArea.warehouseIdValue, void 0)
+ const tarWareId = normalizeNumber(tarArea.warehouseId ?? tarArea.warehouse_id ?? tarArea.warehouseIdValue, void 0)
+
+ return {
+ ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
+ ? { id: normalizeNumber(formData.id) }
+ : {}),
+ ...(normalizeText(formData.code) ? { code: normalizeText(formData.code) } : {}),
+ ...(formData.type !== undefined && formData.type !== null && formData.type !== ''
+ ? { type: normalizeNumber(formData.type) }
+ : {}),
+ ...(formData.source !== undefined && formData.source !== null && formData.source !== ''
+ ? { source: normalizeNumber(formData.source) }
+ : {}),
+ ...(formData.exceStatus !== undefined && formData.exceStatus !== null && formData.exceStatus !== ''
+ ? { exceStatus: normalizeNumber(formData.exceStatus) }
+ : {}),
+ ...(orgAreaId !== void 0 ? { orgAreaId } : {}),
+ ...(tarAreaId !== void 0 ? { tarAreaId } : {}),
+ ...(orgWareId !== void 0 ? { orgWareId } : {}),
+ ...(tarWareId !== void 0 ? { tarWareId } : {}),
+ ...(normalizeText(orgArea.name || orgArea.areaName) ? { orgAreaName: normalizeText(orgArea.name || orgArea.areaName) } : {}),
+ ...(normalizeText(tarArea.name || tarArea.areaName) ? { tarAreaName: normalizeText(tarArea.name || tarArea.areaName) } : {}),
+ ...(normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) ? { orgWareName: normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) } : {}),
+ ...(normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) ? { tarWareName: normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) } : {}),
+ ...(formData.status !== undefined && formData.status !== null && formData.status !== ''
+ ? { status: normalizeNumber(formData.status) }
+ : { status: 1 }),
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+function resolveAreaText(record = {}, key) {
+ return normalizeText(record[`${key}AreaName$`] || record[`${key}AreaName`] || record[`${key}AreaId$`] || record[`${key}AreaId`])
+}
+
+function resolveWarehouseText(record = {}, key) {
+ return normalizeText(record[`${key}WareName$`] || record[`${key}WareName`] || record[`${key}WareId$`] || record[`${key}WareId`])
+}
+
+export function normalizeTransferRow(record = {}) {
+ const statusMeta = metaByValue(record.statusBool ?? record.status, TRANSFER_STATUS_META, '鏈煡')
+ const exceStatusMeta = metaByValue(record.exceStatus, TRANSFER_EXCE_STATUS_META, record.exceStatusText)
+ const sourceMeta = metaByValue(record.source, TRANSFER_SOURCE_META, record.sourceText)
+ return {
+ ...record,
+ id: record.id ?? null,
+ code: normalizeText(record.code) || '--',
+ typeLabel: normalizeText(record['type$'] || record.type) || '--',
+ sourceText: sourceMeta.text,
+ sourceTagType: sourceMeta.type,
+ exceStatusText: normalizeText(record['exceStatus$'] || record.exceStatusText) || exceStatusMeta.text,
+ exceStatusTagType: exceStatusMeta.type,
+ orgWareName: resolveWarehouseText(record, 'org') || '--',
+ tarWareName: resolveWarehouseText(record, 'tar') || '--',
+ orgAreaName: resolveAreaText(record, 'org') || '--',
+ tarAreaName: resolveAreaText(record, 'tar') || '--',
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function normalizeTransferDetailRecord(record = {}) {
+ return normalizeTransferRow(record)
+}
+
+export function normalizeTransferOrderRow(record = {}) {
+ const statusMeta = metaByValue(record.statusBool ?? record.status, TRANSFER_STATUS_META, '鏈煡')
+ const exceStatusMeta = metaByValue(record.exceStatus, TRANSFER_EXCE_STATUS_META, record.exceStatusText)
+ return {
+ ...record,
+ id: record.id ?? null,
+ code: normalizeText(record.code) || '--',
+ poCode: normalizeText(record.poCode) || '--',
+ typeLabel: normalizeText(record['type$'] || record.type) || '--',
+ wkTypeLabel: normalizeText(record['wkType$'] || record.wkType) || '--',
+ exceStatusText: normalizeText(record['exceStatus$'] || record.exceStatusText) || exceStatusMeta.text,
+ exceStatusTagType: exceStatusMeta.type,
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ anfme: record.anfme ?? '--',
+ workQty: record.workQty ?? '--',
+ qty: record.qty ?? '--',
+ stationId: normalizeText(record.stationId) || '--',
+ businessTimeText: normalizeText(record['businessTime$'] || record.businessTimeText || record.businessTime) || '--',
+ createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function buildTransferPrintRows(records = []) {
+ return Array.isArray(records) ? records.map((record) => normalizeTransferRow(record)) : []
+}
+
+export function buildTransferOrderPrintRows(records = []) {
+ return Array.isArray(records) ? records.map((record) => normalizeTransferOrderRow(record)) : []
+}
+
+export function buildTransferReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = TRANSFER_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: TRANSFER_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...TRANSFER_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function getTransferActionList(row = {}) {
+ const normalizedRow = normalizeTransferRow(row)
+ const actions = [
+ { key: 'view', label: '鏌ョ湅璇︽儏', icon: 'ri:eye-line' },
+ { key: 'edit', label: '缂栬緫', icon: 'ri:pencil-line' }
+ ]
+ if (Number(normalizedRow.exceStatus) === 0) {
+ actions.push({ key: 'publish', label: '涓嬪彂鎵ц', icon: 'ri:send-plane-line' })
+ actions.push({ key: 'delete', label: '鍒犻櫎', icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
+ }
+ return actions
+}
+
+export function resolveTransferAreaOptions(records = []) {
+ if (!Array.isArray(records)) return []
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') return null
+ const value = normalizeNumber(item.id ?? item.value, void 0)
+ if (value === void 0) return null
+ return {
+ value,
+ label: normalizeText(item.name || item.areaName || item.code || item.warehouseId$ || item.warehouseName || `搴撳尯 ${value}`),
+ raw: item
+ }
+ })
+ .filter(Boolean)
+}
+
+export function resolveTransferTypeOptions(records = []) {
+ if (!Array.isArray(records)) return []
+ return records
+ .map((item) => {
+ if (!item || typeof item !== 'object') return null
+ const value = normalizeNumber(item.value ?? item.id, void 0)
+ if (value === void 0) return null
+ return {
+ value,
+ label: normalizeText(item.label || item.name || item.dictLabel || `绫诲瀷 ${value}`)
+ }
+ })
+ .filter(Boolean)
+}
diff --git a/rsf-design/src/views/orders/transfer/transferTable.columns.js b/rsf-design/src/views/orders/transfer/transferTable.columns.js
new file mode 100644
index 0000000..aa3760d
--- /dev/null
+++ b/rsf-design/src/views/orders/transfer/transferTable.columns.js
@@ -0,0 +1,199 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getTransferActionList } from './transferPage.helpers.js'
+
+export function createTransferTableColumns({ handleActionClick } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: '璋冩嫧鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'typeLabel',
+ label: '璋冩嫧绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeLabel || '--'
+ },
+ {
+ prop: 'sourceText',
+ label: '鏉ユ簮',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) =>
+ h(ElTag, { type: row.sourceTagType || 'info', effect: 'light' }, () => row.sourceText || '--')
+ },
+ {
+ prop: 'orgWareName',
+ label: '婧愪粨搴�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.orgWareName || '--'
+ },
+ {
+ prop: 'tarWareName',
+ label: '鐩爣浠撳簱',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.tarWareName || '--'
+ },
+ {
+ prop: 'orgAreaName',
+ label: '婧愬簱鍖�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.orgAreaName || '--'
+ },
+ {
+ prop: 'tarAreaName',
+ label: '鐩爣搴撳尯',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.tarAreaName || '--'
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鎵ц鐘舵��',
+ minWidth: 120,
+ formatter: (row) =>
+ h(ElTag, { type: row.exceStatusTagType || 'info', effect: 'light' }, () => row.exceStatusText || '--')
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '--')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 220,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: getTransferActionList(row),
+ onClick: (item) => handleActionClick?.(item, row)
+ })
+ }
+ ]
+}
+
+export function createTransferOrderTableColumns() {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: '鍏宠仈鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'poCode',
+ label: '璋冩嫧鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.poCode || '--'
+ },
+ {
+ prop: 'typeLabel',
+ label: '鍗曟嵁绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeLabel || '--'
+ },
+ {
+ prop: 'wkTypeLabel',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.wkTypeLabel || '--'
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鎵ц鐘舵��',
+ minWidth: 120,
+ formatter: (row) =>
+ h(ElTag, { type: row.exceStatusTagType || 'info', effect: 'light' }, () => row.exceStatusText || '--')
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '--')
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸插畬鎴愭暟閲�',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'stationId',
+ label: '绔欑偣',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.stationId || '--'
+ },
+ {
+ prop: 'businessTimeText',
+ label: '涓氬姟鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.businessTimeText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/wait-pakin-item-log/index.vue b/rsf-design/src/views/orders/wait-pakin-item-log/index.vue
new file mode 100644
index 0000000..7973ab0
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-item-log/index.vue
@@ -0,0 +1,329 @@
+<template>
+ <div class="wait-pakin-item-log-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <WaitPakinItemLogDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import {
+ fetchExportWaitPakinItemLogReport,
+ fetchWaitPakinItemLogDetail,
+ fetchWaitPakinItemLogMany,
+ fetchWaitPakinItemLogPage
+ } from '@/api/wait-pakin-item-log'
+ import WaitPakinItemLogDetailDrawer from './modules/wait-pakin-item-log-detail-drawer.vue'
+ import { createWaitPakinItemLogTableColumns } from './waitPakinItemLogTable.columns'
+ import {
+ WAIT_PAKIN_ITEM_LOG_REPORT_STYLE,
+ WAIT_PAKIN_ITEM_LOG_REPORT_TITLE,
+ buildWaitPakinItemLogPageQueryParams,
+ buildWaitPakinItemLogPrintRows,
+ buildWaitPakinItemLogReportMeta,
+ buildWaitPakinItemLogSearchParams,
+ createWaitPakinItemLogSearchState,
+ getWaitPakinItemLogPaginationKey,
+ getWaitPakinItemLogStatusOptions,
+ normalizeWaitPakinItemLogRow
+ } from './waitPakinItemLogPage.helpers'
+
+ defineOptions({ name: 'WaitPakinItemLog' })
+
+ const userStore = useUserStore()
+ const reportTitle = WAIT_PAKIN_ITEM_LOG_REPORT_TITLE
+ const searchForm = ref(createWaitPakinItemLogSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ棩蹇桰D/缁勬墭鍗旾D/鐗╂枡缂栫爜/璺熻釜鐮�'
+ }
+ },
+ {
+ label: '鏃ュ織ID',
+ key: 'logId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ棩蹇桰D'
+ }
+ },
+ {
+ label: '缁勬墭鍗旾D',
+ key: 'pakinId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ粍鎵樺崟ID'
+ }
+ },
+ {
+ label: '缁勬墭鏄庣粏ID',
+ key: 'pakinItemId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ粍鎵樻槑缁咺D'
+ }
+ },
+ {
+ label: 'ASN鍗曞彿',
+ key: 'asnCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏SN鍗曞彿'
+ }
+ },
+ {
+ label: 'ASN鏄庣粏ID',
+ key: 'asnItemId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏SN鏄庣粏ID'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '璺熻釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ窡韪爜'
+ }
+ },
+ {
+ label: '鎵规鍙�',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆″彿'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getWaitPakinItemLogStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ const reportQueryParams = computed(() => buildWaitPakinItemLogSearchParams(searchForm.value))
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchWaitPakinItemLogPage,
+ apiParams: buildWaitPakinItemLogPageQueryParams(searchForm.value),
+ paginationKey: getWaitPakinItemLogPaginationKey(),
+ columnsFactory: () =>
+ createWaitPakinItemLogTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeWaitPakinItemLogRow(item)) : []
+ }
+ })
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeWaitPakinItemLogRow(
+ await guardRequestWithMessage(fetchWaitPakinItemLogDetail(row.id), {}, {
+ timeoutMessage: '缁勬墭鏄庣粏鍘嗗彶妗h鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ )
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇缁勬墭鏄庣粏鍘嗗彶妗h鎯呭け璐�')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildWaitPakinItemLogPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createWaitPakinItemLogSearchState())
+ resetSearchParams()
+ }
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...WAIT_PAKIN_ITEM_LOG_REPORT_STYLE
+ }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchWaitPakinItemLogMany(payload.ids)).records
+ }
+
+ return defaultResponseAdapter(
+ await fetchWaitPakinItemLogPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'wait-pakin-item-log.xlsx',
+ requestExport: (payload) =>
+ fetchExportWaitPakinItemLogReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildWaitPakinItemLogPrintRows(records),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildWaitPakinItemLogReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation ||
+ WAIT_PAKIN_ITEM_LOG_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin-item-log/modules/wait-pakin-item-log-detail-drawer.vue b/rsf-design/src/views/orders/wait-pakin-item-log/modules/wait-pakin-item-log-detail-drawer.vue
new file mode 100644
index 0000000..e789a2a
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-item-log/modules/wait-pakin-item-log-detail-drawer.vue
@@ -0,0 +1,70 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="缁勬墭鏄庣粏鍘嗗彶妗h鎯�"
+ size="1180px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鏃ュ織ID">{{ detail.logId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁勬墭鍗旾D">{{ detail.pakinId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁勬墭鏄庣粏ID">{{ detail.pakinItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ASN鍗曞彿">{{ detail.asnCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ASN鏄庣粏ID">{{ detail.asnItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璺熻釜鐮�">{{ detail.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="鏁伴噺淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц涓暟閲�">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸插畬鎴愭暟閲�">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规鍙�">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡ID">{{ detail.matnrId || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin-item-log/waitPakinItemLogPage.helpers.js b/rsf-design/src/views/orders/wait-pakin-item-log/waitPakinItemLogPage.helpers.js
new file mode 100644
index 0000000..e71ae65
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-item-log/waitPakinItemLogPage.helpers.js
@@ -0,0 +1,167 @@
+export const WAIT_PAKIN_ITEM_LOG_REPORT_TITLE = '缁勬墭鏄庣粏鍘嗗彶妗f姤琛�'
+export const WAIT_PAKIN_ITEM_LOG_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+function normalizeText(value, fallback = '--') {
+ if (value === null || value === undefined || value === '') {
+ return fallback
+ }
+ const text = String(value).trim()
+ return text || fallback
+}
+
+function normalizeNumber(value, fallback = '--') {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isNaN(numericValue) ? fallback : numericValue
+}
+
+export function createWaitPakinItemLogSearchState() {
+ return {
+ condition: '',
+ logId: '',
+ pakinId: '',
+ pakinItemId: '',
+ asnId: '',
+ asnCode: '',
+ asnItemId: '',
+ trackCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ status: '',
+ memo: ''
+ }
+}
+
+export function getWaitPakinItemLogPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getWaitPakinItemLogStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getWaitPakinItemLogStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildWaitPakinItemLogSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'asnCode',
+ 'trackCode',
+ 'matnrCode',
+ 'maktx',
+ 'batch',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key], '')
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['logId', 'pakinId', 'pakinItemId', 'asnId', 'asnItemId', 'status'].forEach((key) => {
+ if (params[key] === '' || params[key] === null || params[key] === undefined) {
+ return
+ }
+ const numericValue = Number(params[key])
+ if (!Number.isNaN(numericValue)) {
+ result[key] = numericValue
+ }
+ })
+
+ return result
+}
+
+export function buildWaitPakinItemLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWaitPakinItemLogSearchParams(params)
+ }
+}
+
+export function normalizeWaitPakinItemLogRow(record = {}) {
+ const statusMeta = getWaitPakinItemLogStatusMeta(record.statusBool ?? record.status)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ logId: normalizeText(record.logId),
+ pakinId: normalizeText(record.pakinId),
+ pakinItemId: normalizeText(record.pakinItemId),
+ asnId: normalizeText(record.asnId),
+ asnCode: normalizeText(record.asnCode),
+ asnItemId: normalizeText(record.asnItemId),
+ trackCode: normalizeText(record.trackCode),
+ maktx: normalizeText(record.maktx),
+ matnrId: normalizeText(record.matnrId),
+ matnrCode: normalizeText(record.matnrCode),
+ anfme: normalizeNumber(record.anfme),
+ workQty: normalizeNumber(record.workQty),
+ qty: normalizeNumber(record.qty),
+ unit: normalizeText(record.unit),
+ batch: normalizeText(record.batch),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo),
+ createByText: normalizeText(record['createBy$'] || record.createByText, '--'),
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime, '--'),
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText, '--'),
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime, '--')
+ }
+}
+
+export function buildWaitPakinItemLogPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeWaitPakinItemLogRow(record))
+}
+
+export function buildWaitPakinItemLogReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = WAIT_PAKIN_ITEM_LOG_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: WAIT_PAKIN_ITEM_LOG_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...WAIT_PAKIN_ITEM_LOG_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/wait-pakin-item-log/waitPakinItemLogTable.columns.js b/rsf-design/src/views/orders/wait-pakin-item-log/waitPakinItemLogTable.columns.js
new file mode 100644
index 0000000..9105aa1
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-item-log/waitPakinItemLogTable.columns.js
@@ -0,0 +1,131 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+import { getWaitPakinItemLogStatusMeta } from './waitPakinItemLogPage.helpers'
+
+export function createWaitPakinItemLogTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'logId',
+ label: '鏃ュ織ID',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.logId || '--'
+ },
+ {
+ prop: 'pakinId',
+ label: '缁勬墭鍗旾D',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.pakinId || '--'
+ },
+ {
+ prop: 'pakinItemId',
+ label: '缁勬墭鏄庣粏ID',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.pakinItemId || '--'
+ },
+ {
+ prop: 'asnCode',
+ label: 'ASN鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.asnCode || '--'
+ },
+ {
+ prop: 'asnItemId',
+ label: 'ASN鏄庣粏ID',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.asnItemId || '--'
+ },
+ {
+ prop: 'trackCode',
+ label: '璺熻釜鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.trackCode || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸插畬鎴愭暟閲�',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规鍙�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getWaitPakinItemLogStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 96,
+ fixed: 'right',
+ align: 'center',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/wait-pakin-item/index.vue b/rsf-design/src/views/orders/wait-pakin-item/index.vue
new file mode 100644
index 0000000..16e289d
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-item/index.vue
@@ -0,0 +1,333 @@
+<template>
+ <div class="wait-pakin-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <WaitPakinItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import {
+ fetchExportWaitPakinItemReport,
+ fetchWaitPakinItemDetail,
+ fetchWaitPakinItemMany,
+ fetchWaitPakinItemPage
+ } from '@/api/wait-pakin-item'
+ import WaitPakinItemDetailDrawer from './modules/wait-pakin-item-detail-drawer.vue'
+ import { createWaitPakinItemTableColumns } from './waitPakinItemTable.columns'
+ import {
+ WAIT_PAKIN_ITEM_REPORT_STYLE,
+ WAIT_PAKIN_ITEM_REPORT_TITLE,
+ buildWaitPakinItemPageQueryParams,
+ buildWaitPakinItemPrintRows,
+ buildWaitPakinItemReportMeta,
+ buildWaitPakinItemSearchParams,
+ createWaitPakinItemSearchState,
+ getWaitPakinItemPaginationKey,
+ getWaitPakinItemStatusOptions,
+ normalizeWaitPakinItemRow
+ } from './waitPakinItemPage.helpers'
+
+ defineOptions({ name: 'WaitPakinItem' })
+
+ const userStore = useUserStore()
+ const reportTitle = WAIT_PAKIN_ITEM_REPORT_TITLE
+ const searchForm = ref(createWaitPakinItemSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ粍鎵樺崟ID/ASN鍗曞彿/鐗╂枡缂栫爜/璺熻釜鐮�'
+ }
+ },
+ {
+ label: '缁勬墭鍗旾D',
+ key: 'pakinId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ粍鎵樺崟ID'
+ }
+ },
+ {
+ label: '璁㈠崟绫诲瀷',
+ key: 'type',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鍗曠被鍨�'
+ }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'wkType',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�'
+ }
+ },
+ {
+ label: 'ASN鍗曞彿',
+ key: 'asnCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏SN鍗曞彿'
+ }
+ },
+ {
+ label: 'ASN鏄庣粏ID',
+ key: 'asnItemId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏SN鏄庣粏ID'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '璺熻釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ窡韪爜'
+ }
+ },
+ {
+ label: '鎵规鍙�',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆″彿'
+ }
+ },
+ {
+ label: '璐ㄦ缁撴灉',
+ key: 'isptResult',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鍚堟牸', value: 1 },
+ { label: '涓嶅悎鏍�', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getWaitPakinItemStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ const reportQueryParams = computed(() => buildWaitPakinItemSearchParams(searchForm.value))
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchWaitPakinItemPage,
+ apiParams: buildWaitPakinItemPageQueryParams(searchForm.value),
+ paginationKey: getWaitPakinItemPaginationKey(),
+ columnsFactory: () =>
+ createWaitPakinItemTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeWaitPakinItemRow(item)) : []
+ }
+ })
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchWaitPakinItemDetail(row.id), {}, {
+ timeoutMessage: '缁勬墭鏄庣粏璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeWaitPakinItemRow({
+ ...row,
+ ...(detail || {}),
+ extendFields: detail?.extendFields ?? row.extendFields ?? {}
+ })
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇缁勬墭鏄庣粏璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildWaitPakinItemPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createWaitPakinItemSearchState())
+ resetSearchParams()
+ }
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...WAIT_PAKIN_ITEM_REPORT_STYLE
+ }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchWaitPakinItemMany(payload.ids)).records
+ }
+
+ return defaultResponseAdapter(
+ await fetchWaitPakinItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'wait-pakin-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportWaitPakinItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildWaitPakinItemPrintRows(records),
+ buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildWaitPakinItemReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || WAIT_PAKIN_ITEM_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin-item/modules/wait-pakin-item-detail-drawer.vue b/rsf-design/src/views/orders/wait-pakin-item/modules/wait-pakin-item-detail-drawer.vue
new file mode 100644
index 0000000..bc126bc
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-item/modules/wait-pakin-item-detail-drawer.vue
@@ -0,0 +1,102 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="缁勬墭鏄庣粏璇︽儏"
+ size="1180px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="缁勬墭鏄庣粏ID">{{ detail.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁勬墭鍗旾D">{{ detail.pakinId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ASN鍗曞彿">{{ detail.asnCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="ASN鏄庣粏ID">{{ detail.asnItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁㈠崟绫诲瀷">{{ detail.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.wkTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐ㄦ缁撴灉">{{ detail.isptResultText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="鐗╂枡淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鐗╂枡ID">{{ detail.matnrId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璺熻釜鐮�">{{ detail.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规鍙�">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц涓暟閲�">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸插畬鎴愭暟閲�">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍩虹鍗曚綅">{{ detail.baseUnit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璁㈠崟鏉ユ簮">{{ detail.source || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴琛屽彿">{{ detail.platItemId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛璁㈠崟鍙�">{{ detail.platOrderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ュ崟鍙�">{{ detail.platWorkCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="椤圭洰鍙�">{{ detail.projectCode || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions v-if="extendFieldEntries.length > 0" title="鎵╁睍瀛楁" :column="2" border>
+ <ElDescriptionsItem
+ v-for="entry in extendFieldEntries"
+ :key="entry.key"
+ :label="entry.key"
+ >
+ {{ entry.value || '--' }}
+ </ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ const extendFieldEntries = computed(() => {
+ const extendFields = props.detail?.extendFields
+ if (!extendFields || typeof extendFields !== 'object' || Array.isArray(extendFields)) {
+ return []
+ }
+ return Object.entries(extendFields)
+ .map(([key, value]) => ({
+ key: String(key ?? '').trim(),
+ value: String(value ?? '').trim()
+ }))
+ .filter((entry) => entry.key)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin-item/waitPakinItemPage.helpers.js b/rsf-design/src/views/orders/wait-pakin-item/waitPakinItemPage.helpers.js
new file mode 100644
index 0000000..03fb976
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-item/waitPakinItemPage.helpers.js
@@ -0,0 +1,220 @@
+export const WAIT_PAKIN_ITEM_REPORT_TITLE = '缁勬墭鏄庣粏鎶ヨ〃'
+export const WAIT_PAKIN_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+const ISPT_RESULT_OPTIONS = [
+ { label: '鍚堟牸', value: 1 },
+ { label: '涓嶅悎鏍�', value: 0 }
+]
+
+function normalizeText(value, fallback = '--') {
+ if (value === null || value === undefined || value === '') {
+ return fallback
+ }
+ const text = String(value).trim()
+ return text || fallback
+}
+
+function normalizeNumber(value, fallback = '--') {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isNaN(numericValue) ? fallback : numericValue
+}
+
+function buildObjectEntries(record = {}) {
+ return Object.fromEntries(
+ Object.entries(record)
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
+ .map(([key, value]) => [key, normalizeText(value, '')])
+ .filter(([, value]) => value !== '')
+ )
+}
+
+export function createWaitPakinItemSearchState() {
+ return {
+ condition: '',
+ pakinId: '',
+ type: '',
+ wkType: '',
+ asnCode: '',
+ asnItemId: '',
+ matnrCode: '',
+ trackCode: '',
+ batch: '',
+ isptResult: '',
+ status: '',
+ memo: '',
+ fieldsIndex: ''
+ }
+}
+
+export function getWaitPakinItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getWaitPakinItemStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getWaitPakinItemIsptResultOptions() {
+ return ISPT_RESULT_OPTIONS
+}
+
+export function getWaitPakinItemStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildWaitPakinItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'type',
+ 'asnCode',
+ 'matnrCode',
+ 'trackCode',
+ 'batch',
+ 'memo',
+ 'fieldsIndex'
+ ].forEach((key) => {
+ const value = normalizeText(params[key], '')
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['pakinId', 'asnItemId', 'wkType', 'isptResult', 'status'].forEach((key) => {
+ if (params[key] === '' || params[key] === null || params[key] === undefined) {
+ return
+ }
+ const numericValue = Number(params[key])
+ if (!Number.isNaN(numericValue)) {
+ result[key] = numericValue
+ }
+ })
+
+ return result
+}
+
+export function buildWaitPakinItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWaitPakinItemSearchParams(params)
+ }
+}
+
+export function normalizeWaitPakinItemExtendFields(extendFields = {}) {
+ if (!extendFields || typeof extendFields !== 'object' || Array.isArray(extendFields)) {
+ return {}
+ }
+
+ return buildObjectEntries(extendFields)
+}
+
+export function normalizeWaitPakinItemRow(record = {}) {
+ const extendFields = normalizeWaitPakinItemExtendFields(record.extendFields)
+ const extendFieldEntries = Object.entries(extendFields)
+ const statusMeta = getWaitPakinItemStatusMeta(record.statusBool ?? record.status)
+
+ return {
+ ...record,
+ id: record.id ?? null,
+ pakinId: normalizeText(record.pakinId),
+ typeText: normalizeText(record['type$'] || record.type, '--'),
+ wkTypeText: normalizeText(record['wkType$'] || record.wkType, '--'),
+ source: normalizeText(record.source, '--'),
+ isptResultText: normalizeText(record['isptResult$'] || record.isptResultText, '--'),
+ asnId: normalizeText(record.asnId),
+ asnCode: normalizeText(record.asnCode),
+ asnItemId: normalizeText(record.asnItemId),
+ platItemId: normalizeText(record.platItemId, '--'),
+ platOrderCode: normalizeText(record.platOrderCode, '--'),
+ platWorkCode: normalizeText(record.platWorkCode, '--'),
+ projectCode: normalizeText(record.projectCode, '--'),
+ maktx: normalizeText(record.maktx),
+ matnrId: normalizeText(record.matnrId),
+ matnrCode: normalizeText(record.matnrCode),
+ anfme: normalizeNumber(record.anfme),
+ workQty: normalizeNumber(record.workQty),
+ qty: normalizeNumber(record.qty),
+ unit: normalizeText(record.unit),
+ fieldsIndex: normalizeText(record.fieldsIndex, '--'),
+ batch: normalizeText(record.batch),
+ baseUnit: normalizeText(record.baseUnit, '--'),
+ useOrgId: normalizeText(record.useOrgId, '--'),
+ useOrgName: normalizeText(record.useOrgName, '--'),
+ erpClsId: normalizeText(record.erpClsId, '--'),
+ priceUnitId: normalizeText(record.priceUnitId, '--'),
+ inStockType: normalizeText(record.inStockType, '--'),
+ ownerTypeId: normalizeText(record.ownerTypeId, '--'),
+ ownerId: normalizeText(record.ownerId, '--'),
+ ownerName: normalizeText(record.ownerName, '--'),
+ keeperTypeId: normalizeText(record.keeperTypeId, '--'),
+ keeperId: normalizeText(record.keeperId, '--'),
+ keeperName: normalizeText(record.keeperName, '--'),
+ targetWarehouseId: normalizeText(record.targetWarehouseId, '--'),
+ sourceWarehouseId: normalizeText(record.sourceWarehouseId, '--'),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo),
+ createByText: normalizeText(record['createBy$'] || record.createByText, '--'),
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime, '--'),
+ updateByText: normalizeText(record['updateBy$'] || record.updateByText, '--'),
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime, '--'),
+ extendFields,
+ extendFieldsText: extendFieldEntries.length
+ ? extendFieldEntries.map(([key, value]) => `${key}:${value}`).join('锛�')
+ : '--'
+ }
+}
+
+export function buildWaitPakinItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeWaitPakinItemRow(record))
+}
+
+export function buildWaitPakinItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = WAIT_PAKIN_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: WAIT_PAKIN_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...WAIT_PAKIN_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/wait-pakin-item/waitPakinItemTable.columns.js b/rsf-design/src/views/orders/wait-pakin-item/waitPakinItemTable.columns.js
new file mode 100644
index 0000000..33ea4b5
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-item/waitPakinItemTable.columns.js
@@ -0,0 +1,145 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+import { getWaitPakinItemStatusMeta } from './waitPakinItemPage.helpers'
+
+export function createWaitPakinItemTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'pakinId',
+ label: '缁勬墭鍗旾D',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.pakinId || '--'
+ },
+ {
+ prop: 'asnCode',
+ label: 'ASN鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.asnCode || '--'
+ },
+ {
+ prop: 'asnItemId',
+ label: 'ASN鏄庣粏ID',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.asnItemId || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'trackCode',
+ label: '璺熻釜鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.trackCode || '--'
+ },
+ {
+ prop: 'typeText',
+ label: '璁㈠崟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.typeText || '--'
+ },
+ {
+ prop: 'wkTypeText',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.wkTypeText || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.workQty ?? '--'
+ },
+ {
+ prop: 'qty',
+ label: '宸插畬鎴愭暟閲�',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.qty ?? '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规鍙�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'isptResultText',
+ label: '璐ㄦ缁撴灉',
+ minWidth: 110,
+ showOverflowTooltip: true,
+ formatter: (row) => row.isptResultText || '--'
+ },
+ {
+ prop: 'extendFieldsText',
+ label: '鎵╁睍瀛楁',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.extendFieldsText || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getWaitPakinItemStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 96,
+ fixed: 'right',
+ align: 'center',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/wait-pakin-log/index.vue b/rsf-design/src/views/orders/wait-pakin-log/index.vue
new file mode 100644
index 0000000..b543b16
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-log/index.vue
@@ -0,0 +1,339 @@
+<template>
+ <div class="wait-pakin-log-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <WaitPakinLogDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :data="detailTableData"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @refresh="loadDetailResources"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchExportWaitPakinLogReport,
+ fetchGetWaitPakinLogDetail,
+ fetchGetWaitPakinLogMany,
+ fetchWaitPakinItemLogPage,
+ fetchWaitPakinLogPage
+ } from '@/api/wait-pakin-log'
+ import WaitPakinLogDetailDrawer from './modules/wait-pakin-log-detail-drawer.vue'
+ import {
+ createWaitPakinItemLogColumns,
+ createWaitPakinLogTableColumns
+ } from './waitPakinLogTable.columns'
+ import {
+ WAIT_PAKIN_LOG_REPORT_STYLE,
+ WAIT_PAKIN_LOG_REPORT_TITLE,
+ buildWaitPakinLogDetailQueryParams,
+ buildWaitPakinLogPageQueryParams,
+ buildWaitPakinLogPrintRows,
+ buildWaitPakinLogReportMeta,
+ buildWaitPakinLogSearchParams,
+ createWaitPakinLogSearchState,
+ getWaitPakinIoStatusOptions,
+ normalizeWaitPakinItemLogRow,
+ normalizeWaitPakinLogRow
+ } from './waitPakinLogPage.helpers'
+
+ defineOptions({ name: 'WaitPakinLog' })
+
+ const userStore = useUserStore()
+ const reportTitle = WAIT_PAKIN_LOG_REPORT_TITLE
+ const searchForm = ref(createWaitPakinLogSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const detailTableData = ref([])
+ const activeLogId = ref(null)
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildWaitPakinLogSearchParams(searchForm.value))
+ const detailColumns = computed(() => createWaitPakinItemLogColumns())
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ粍鎵樼紪鐮�/瀹瑰櫒鐮�'
+ }
+ },
+ {
+ label: '缁勬墭鍗旾D',
+ key: 'pakinId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ粍鎵樺崟ID'
+ }
+ },
+ {
+ label: '缁勬墭缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ粍鎵樼紪鐮�'
+ }
+ },
+ {
+ label: '瀹瑰櫒鐮�',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鍣ㄧ爜'
+ }
+ },
+ {
+ label: '缁勬墭鐘舵��',
+ key: 'ioStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getWaitPakinIoStatusOptions()
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ activeLogId.value = row.id
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ await loadDetailResources()
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchWaitPakinLogPage,
+ apiParams: buildWaitPakinLogPageQueryParams(searchForm.value),
+ columnsFactory: () =>
+ createWaitPakinLogTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeWaitPakinLogRow(item)) : []
+ }
+ })
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadDetailResources() {
+ if (!activeLogId.value) {
+ return
+ }
+
+ detailLoading.value = true
+ try {
+ const [detailResponse, itemResponse] = await Promise.all([
+ guardRequestWithMessage(
+ fetchGetWaitPakinLogDetail(activeLogId.value),
+ {},
+ {
+ timeoutMessage: '缁勬墭鍘嗗彶璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ ),
+ guardRequestWithMessage(
+ fetchWaitPakinItemLogPage(
+ buildWaitPakinLogDetailQueryParams({
+ logId: activeLogId.value,
+ current: detailPagination.current,
+ pageSize: detailPagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: detailPagination.current,
+ size: detailPagination.size
+ },
+ {
+ timeoutMessage: '缁勬墭鍘嗗彶鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ ])
+
+ detailData.value = normalizeWaitPakinLogRow(detailResponse)
+ detailTableData.value = Array.isArray(itemResponse?.records)
+ ? itemResponse.records.map((item) => normalizeWaitPakinItemLogRow(item))
+ : []
+ updatePaginationState(
+ detailPagination,
+ itemResponse,
+ detailPagination.current,
+ detailPagination.size
+ )
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ detailTableData.value = []
+ ElMessage.error(error?.message || '鑾峰彇缁勬墭鍘嗗彶璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildWaitPakinLogSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createWaitPakinLogSearchState()
+ resetSearchParams()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ detailPagination.current = 1
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'wait-pakin-log.xlsx',
+ requestExport: (payload) =>
+ fetchExportWaitPakinLogReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords: async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetWaitPakinLogMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchWaitPakinLogPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0
+ ? Number(pagination.total)
+ : Number(payload?.pageSize) || 20
+ })
+ ).records
+ },
+ buildPreviewRows: (records) => buildWaitPakinLogPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...WAIT_PAKIN_LOG_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildWaitPakinLogReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || WAIT_PAKIN_LOG_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin-log/modules/wait-pakin-log-detail-drawer.vue b/rsf-design/src/views/orders/wait-pakin-log/modules/wait-pakin-log-detail-drawer.vue
new file mode 100644
index 0000000..d6eef8a
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-log/modules/wait-pakin-log-detail-drawer.vue
@@ -0,0 +1,59 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="缁勬墭鍘嗗彶妗h鎯�"
+ size="88%"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="缁勬墭鍗旾D">{{ detail.pakinId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁勬墭缂栫爜">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹瑰櫒鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁勬墭鐘舵��">{{ detail.ioStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁勬墭鏁伴噺">{{ detail.anfme ?? 0 }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{
+ detail.updateTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{
+ detail.createTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="flex items-center justify-between">
+ <div class="text-sm text-[var(--art-gray-600)]">鍘嗗彶鏄庣粏锛堢墿鏂欑紪鐮�/鐗╂枡鍚嶇О/璺熻釜鐮侊級</div>
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'WaitPakinLogDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin-log/waitPakinLogPage.helpers.js b/rsf-design/src/views/orders/wait-pakin-log/waitPakinLogPage.helpers.js
new file mode 100644
index 0000000..7489fa2
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-log/waitPakinLogPage.helpers.js
@@ -0,0 +1,165 @@
+export const WAIT_PAKIN_LOG_REPORT_TITLE = '缁勬墭鍘嗗彶妗f姤琛�'
+export const WAIT_PAKIN_LOG_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const IO_STATUS_MAP = {
+ 0: { label: '寰呭叆搴�', tagType: 'info' },
+ 1: { label: '鍏ュ簱涓�', tagType: 'warning' },
+ 2: { label: '浠诲姟鎵ц涓�', tagType: 'warning' },
+ 3: { label: '浠诲姟瀹屾垚', tagType: 'success' }
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+function getIoStatusConfig(status, statusText) {
+ const numericStatus = Number(status)
+ const fallback = IO_STATUS_MAP[numericStatus] || {
+ label: statusText || '-',
+ tagType: 'info'
+ }
+ return {
+ label: statusText || fallback.label,
+ tagType: fallback.tagType
+ }
+}
+
+export function createWaitPakinLogSearchState() {
+ return {
+ condition: '',
+ pakinId: '',
+ code: '',
+ barcode: '',
+ ioStatus: ''
+ }
+}
+
+export function buildWaitPakinLogSearchParams(params = {}) {
+ const result = {}
+
+ ;['condition', 'code', 'barcode'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.pakinId !== '' && params.pakinId !== undefined && params.pakinId !== null) {
+ result.pakinId = Number(params.pakinId)
+ }
+
+ if (params.ioStatus !== '' && params.ioStatus !== undefined && params.ioStatus !== null) {
+ result.ioStatus = Number(params.ioStatus)
+ }
+
+ return result
+}
+
+export function buildWaitPakinLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWaitPakinLogSearchParams(params)
+ }
+}
+
+export function buildWaitPakinLogDetailQueryParams(params = {}) {
+ return {
+ logId: params.logId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function normalizeWaitPakinLogRow(record = {}) {
+ const ioStatusConfig = getIoStatusConfig(record.ioStatus, record['ioStatus$'])
+ return {
+ ...record,
+ id: record.id ?? null,
+ pakinId: record.pakinId ?? '-',
+ code: record.code || '-',
+ barcode: record.barcode || '-',
+ anfme: normalizeNumber(record.anfme),
+ ioStatusText: ioStatusConfig.label,
+ ioStatusTagType: ioStatusConfig.tagType,
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createByText: record['createBy$'] || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ statusText:
+ record.statusBool === true || Number(record.status) === 1
+ ? '姝e父'
+ : record.statusBool === false || Number(record.status) === 0
+ ? '鍐荤粨'
+ : '-',
+ memo: record.memo || '-'
+ }
+}
+
+export function normalizeWaitPakinItemLogRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? null,
+ pakinId: record.pakinId ?? '-',
+ pakinItemId: record.pakinItemId ?? '-',
+ asnId: record.asnId ?? '-',
+ asnCode: record.asnCode || '-',
+ asnItemId: record.asnItemId ?? '-',
+ trackCode: record.trackCode || '-',
+ maktx: record.maktx || '-',
+ matnrCode: record.matnrCode || '-',
+ anfme: normalizeNumber(record.anfme),
+ workQty: normalizeNumber(record.workQty),
+ unit: record.unit || '-',
+ qty: normalizeNumber(record.qty),
+ batch: record.batch || '-',
+ updateByText: record['updateBy$'] || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-'
+ }
+}
+
+export function buildWaitPakinLogPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeWaitPakinLogRow(record))
+}
+
+export function buildWaitPakinLogReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = WAIT_PAKIN_LOG_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: WAIT_PAKIN_LOG_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...WAIT_PAKIN_LOG_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function getWaitPakinIoStatusOptions() {
+ return Object.entries(IO_STATUS_MAP).map(([value, item]) => ({
+ label: item.label,
+ value: Number(value)
+ }))
+}
diff --git a/rsf-design/src/views/orders/wait-pakin-log/waitPakinLogTable.columns.js b/rsf-design/src/views/orders/wait-pakin-log/waitPakinLogTable.columns.js
new file mode 100644
index 0000000..0659cd5
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin-log/waitPakinLogTable.columns.js
@@ -0,0 +1,137 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createWaitPakinLogTableColumns({ handleView }) {
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'pakinId',
+ label: '缁勬墭鍗旾D',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'code',
+ label: '缁勬墭缂栫爜',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'barcode',
+ label: '瀹瑰櫒鐮�',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '缁勬墭鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'ioStatusText',
+ label: '缁勬墭鐘舵��',
+ width: 120,
+ formatter: (row) =>
+ h(ElTag, { type: row.ioStatusTagType || 'info', effect: 'light' }, () => row.ioStatusText)
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ })
+ }
+ ]
+}
+
+export function createWaitPakinItemLogColumns() {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'asnCode',
+ label: 'ASN鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'trackCode',
+ label: '璺熻釜鐮�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '宸插畬鎴愭暟閲�',
+ width: 120,
+ align: 'right'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100
+ },
+ {
+ prop: 'batch',
+ label: '鎵规鍙�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/wait-pakin/index.vue b/rsf-design/src/views/orders/wait-pakin/index.vue
new file mode 100644
index 0000000..43bf924
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin/index.vue
@@ -0,0 +1,381 @@
+<template>
+ <div class="wait-pakin-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton
+ type="primary"
+ :disabled="generatableSelectedRows.length === 0"
+ @click="openGenerateTaskDialog(generatableSelectedRows)"
+ v-ripple
+ >
+ 鐢熸垚浠诲姟
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <WaitPakinDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :items-loading="detailItemsLoading"
+ :detail="detailData"
+ :item-rows="detailItemRows"
+ :item-columns="waitPakinItemColumns"
+ />
+
+ <WaitPakinSiteDialog
+ v-model:visible="siteDialogVisible"
+ :selected-rows="taskSourceRows"
+ @select="handleSelectSite"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchDeleteWaitPakin,
+ fetchExportWaitPakinReport,
+ fetchMergeWaitPakinTasks,
+ fetchWaitPakinDetail,
+ fetchWaitPakinItemPage,
+ fetchWaitPakinMany,
+ fetchWaitPakinPage
+ } from '@/api/wait-pakin'
+ import WaitPakinDetailDrawer from './modules/wait-pakin-detail-drawer.vue'
+ import WaitPakinSiteDialog from './modules/wait-pakin-site-dialog.vue'
+ import { createWaitPakinTableColumns } from './waitPakinTable.columns'
+ import {
+ WAIT_PAKIN_REPORT_STYLE,
+ WAIT_PAKIN_REPORT_TITLE,
+ buildWaitPakinMergePayload,
+ buildWaitPakinPageQueryParams,
+ buildWaitPakinPrintRows,
+ buildWaitPakinReportMeta,
+ buildWaitPakinSearchParams,
+ createWaitPakinItemColumns,
+ createWaitPakinSearchState,
+ getWaitPakinIoStatusOptions,
+ getWaitPakinPaginationKey,
+ getWaitPakinStatusOptions,
+ normalizeWaitPakinDetailRecord,
+ normalizeWaitPakinItemRow,
+ normalizeWaitPakinListRow
+ } from './waitPakinPage.helpers'
+
+ defineOptions({ name: 'WaitPakin' })
+
+ const userStore = useUserStore()
+
+ const reportTitle = WAIT_PAKIN_REPORT_TITLE
+ const searchForm = ref(createWaitPakinSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailItemsLoading = ref(false)
+ const detailData = ref({})
+ const detailItemRows = ref([])
+ const waitPakinItemColumns = createWaitPakinItemColumns()
+ const siteDialogVisible = ref(false)
+ const taskSourceRows = ref([])
+
+ const reportQueryParams = computed(() => buildWaitPakinSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ粍鎵樺崟鍙�/瀹瑰櫒鐮�'
+ }
+ },
+ {
+ label: '缁勬墭鍗曞彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ粍鎵樺崟鍙�'
+ }
+ },
+ {
+ label: '瀹瑰櫒鐮�',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鍣ㄧ爜'
+ }
+ },
+ {
+ label: '缁勬墭鐘舵��',
+ key: 'ioStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getWaitPakinIoStatusOptions()
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getWaitPakinStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ const canGenerateTask = (row) => Number(row?.ioStatus) === 1
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshRemove,
+ refreshUpdate
+ } = useTable({
+ core: {
+ apiFn: fetchWaitPakinPage,
+ apiParams: buildWaitPakinPageQueryParams(searchForm.value),
+ paginationKey: getWaitPakinPaginationKey(),
+ columnsFactory: () =>
+ createWaitPakinTableColumns({
+ handleView: openDetail,
+ handleDelete,
+ handleGenerateTask: openGenerateTaskDialog,
+ canDelete: true,
+ canGenerateTask
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeWaitPakinListRow(item))
+ }
+ }
+ })
+
+ const generatableSelectedRows = computed(() =>
+ selectedRows.value.filter((row) => canGenerateTask(row))
+ )
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ async function loadDetailItems(pakinId) {
+ detailItemsLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchWaitPakinItemPage({ pakinId, current: 1, pageSize: 200 }),
+ { records: [] },
+ { timeoutMessage: '缁勬墭鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ detailItemRows.value = defaultResponseAdapter(response).records.map((item) =>
+ normalizeWaitPakinItemRow(item)
+ )
+ } finally {
+ detailItemsLoading.value = false
+ }
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ detailItemRows.value = []
+ try {
+ const detail = await guardRequestWithMessage(
+ fetchWaitPakinDetail(row.id),
+ {},
+ { timeoutMessage: '缁勬墭鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+ detailData.value = normalizeWaitPakinDetailRecord(detail)
+ await loadDetailItems(row.id)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ detailItemRows.value = []
+ ElMessage.error(error?.message || '鑾峰彇缁勬墭鍗曡鎯呭け璐�')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function handleDelete(row) {
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ょ粍鎵樺崟銆�${row.code || row.barcode || row.id}銆嶅悧锛焋,
+ '鍒犻櫎纭',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ )
+ await fetchDeleteWaitPakin(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshRemove()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ function openGenerateTaskDialog(rows) {
+ if (!Array.isArray(rows) || rows.length === 0) {
+ ElMessage.warning('璇峰厛閫夋嫨鍙敓鎴愪换鍔$殑缁勬墭鍗�')
+ return
+ }
+ const validRows = rows.filter((row) => canGenerateTask(row))
+ if (validRows.length === 0) {
+ ElMessage.warning('浠呪�滃叆搴撲腑鈥濈殑缁勬墭鍗曟敮鎸佺敓鎴愪换鍔�')
+ return
+ }
+ taskSourceRows.value = validRows
+ siteDialogVisible.value = true
+ }
+
+ async function handleSelectSite(siteRow) {
+ try {
+ await fetchMergeWaitPakinTasks(buildWaitPakinMergePayload(taskSourceRows.value, siteRow.id))
+ ElMessage.success('鐢熸垚浠诲姟鎴愬姛')
+ siteDialogVisible.value = false
+ taskSourceRows.value = []
+ selectedRows.value = []
+ await refreshUpdate()
+ } catch (error) {
+ ElMessage.error(error?.message || '鐢熸垚浠诲姟澶辫触')
+ }
+ }
+
+ const buildPreviewDialogMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ const response =
+ Array.isArray(payload?.ids) && payload.ids.length > 0
+ ? await fetchWaitPakinMany(payload.ids)
+ : await fetchWaitPakinPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize:
+ Number(pagination.total) > 0
+ ? Number(pagination.total)
+ : Number(payload?.pageSize) || 20
+ })
+ return defaultResponseAdapter(response).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'wait-pakin.xlsx',
+ requestExport: (payload) =>
+ fetchExportWaitPakinReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildWaitPakinPrintRows(records),
+ buildPreviewMeta: buildPreviewDialogMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildWaitPakinReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation:
+ previewMeta.value?.reportStyle?.orientation || WAIT_PAKIN_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildWaitPakinSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createWaitPakinSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin/modules/wait-pakin-detail-drawer.vue b/rsf-design/src/views/orders/wait-pakin/modules/wait-pakin-detail-drawer.vue
new file mode 100644
index 0000000..e154b62
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin/modules/wait-pakin-detail-drawer.vue
@@ -0,0 +1,78 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="缁勬墭鍗曡鎯�"
+ size="1180px"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="12" animated />
+ </div>
+ <div v-else class="space-y-4">
+ <ElDescriptions title="鍩虹淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="缁勬墭鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹瑰櫒鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁勬墭鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁勬墭鐘舵��">{{
+ detail.ioStatusText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏄惁涓嶈壇鍝�">{{
+ detail.flagDefectText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions title="瀹¤淇℃伅" :column="2" border>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{
+ detail.createTimeText || '--'
+ }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{
+ detail.updateTimeText || '--'
+ }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="space-y-3">
+ <div class="flex items-center justify-between">
+ <div class="text-sm font-medium text-[var(--art-gray-900)]">缁勬墭鏄庣粏</div>
+ <ElTag effect="plain">鍏� {{ itemRows.length }} 鏉�</ElTag>
+ </div>
+ <ArtTable :data="itemRows" :columns="itemColumns" :loading="itemsLoading" />
+ </div>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import ArtTable from '@/components/core/tables/art-table/index.vue'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ itemsLoading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ itemRows: { type: Array, default: () => [] },
+ itemColumns: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+
+ function handleVisibleChange(value) {
+ visible.value = value
+ }
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin/modules/wait-pakin-site-dialog.vue b/rsf-design/src/views/orders/wait-pakin/modules/wait-pakin-site-dialog.vue
new file mode 100644
index 0000000..196b61b
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin/modules/wait-pakin-site-dialog.vue
@@ -0,0 +1,189 @@
+<template>
+ <ElDialog
+ title="閫夋嫨绔欑偣"
+ :model-value="visible"
+ width="1080px"
+ align-center
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="space-y-4">
+ <div
+ class="rounded border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-4 py-3 text-sm"
+ >
+ 宸查�夌粍鎵樺崟 {{ selectedRows.length }} 鏉�
+ </div>
+
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columnsWithButton"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </div>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { computed, h, ref, watch } from 'vue'
+ import { ElButton } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchDeviceSitePage } from '@/api/device-site'
+ import {
+ buildWaitPakinSiteSearchParams,
+ createWaitPakinSiteSearchState,
+ normalizeWaitPakinSiteRow
+ } from '../waitPakinPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ selectedRows: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'select'])
+
+ const searchForm = ref(createWaitPakinSiteSearchState())
+
+ const searchItems = computed(() => [
+ {
+ label: '绔欑偣鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐瑰悕绉�'
+ }
+ },
+ {
+ label: '绔欑偣',
+ key: 'site',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ珯鐐�'
+ }
+ },
+ {
+ label: '璁惧浣�',
+ key: 'deviceSite',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ澶囦綅'
+ }
+ }
+ ])
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchDeviceSitePage,
+ apiParams: {
+ current: 1,
+ pageSize: 20,
+ type: 1
+ },
+ paginationKey: {
+ current: 'current',
+ size: 'pageSize'
+ },
+ columnsFactory: () => [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'name', label: '绔欑偣鍚嶇О', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'site', label: '绔欑偣', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'deviceSite', label: '璁惧浣�', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'wcsCode', label: 'WCS缂栫爜', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'label', label: '鏍囩', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'updateTimeText', label: '鏇存柊鏃堕棿', minWidth: 170, showOverflowTooltip: true },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ align: 'center',
+ formatter: (row) => ({
+ label: '閫夋嫨',
+ type: 'primary',
+ plain: true,
+ onClick: () => emit('select', row)
+ })
+ }
+ ]
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeWaitPakinSiteRow(item))
+ }
+ }
+ })
+
+ const columnsWithButton = computed(() =>
+ columns.value.map((column) => {
+ if (column.prop !== 'operation') {
+ return column
+ }
+ return {
+ ...column,
+ formatter: (row) => {
+ const action = column.formatter(row)
+ return h(
+ ElButton,
+ {
+ type: action.type,
+ plain: action.plain,
+ onClick: action.onClick
+ },
+ () => action.label
+ )
+ }
+ }
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildWaitPakinSiteSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createWaitPakinSiteSearchState())
+ resetSearchParams()
+ }
+
+ function handleVisibleChange(value) {
+ emit('update:visible', value)
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ getData()
+ }
+ }
+ )
+</script>
diff --git a/rsf-design/src/views/orders/wait-pakin/waitPakinPage.helpers.js b/rsf-design/src/views/orders/wait-pakin/waitPakinPage.helpers.js
new file mode 100644
index 0000000..9c89d6e
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin/waitPakinPage.helpers.js
@@ -0,0 +1,253 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success', bool: true },
+ 0: { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+const IO_STATUS_OPTIONS = [
+ { label: '寰呭叆搴�', value: 0 },
+ { label: '鍏ュ簱涓�', value: 1 },
+ { label: '浠诲姟鎵ц涓�', value: 2 },
+ { label: '浠诲姟瀹屾垚', value: 3 }
+]
+
+export const WAIT_PAKIN_REPORT_TITLE = '缁勬墭鍗曟姤琛�'
+export const WAIT_PAKIN_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'portrait',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+function normalizeObjectEntries(record = {}) {
+ return Object.fromEntries(
+ Object.entries(record)
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
+ .map(([key, value]) => [key, normalizeText(value)])
+ )
+}
+
+export function createWaitPakinSearchState() {
+ return {
+ condition: '',
+ code: '',
+ barcode: '',
+ ioStatus: '',
+ status: '',
+ memo: ''
+ }
+}
+
+export function getWaitPakinPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getWaitPakinStatusOptions() {
+ return [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+}
+
+export function getWaitPakinIoStatusOptions() {
+ return IO_STATUS_OPTIONS
+}
+
+export function getWaitPakinStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return STATUS_META[1]
+ }
+ if (status === false || Number(status) === 0) {
+ return STATUS_META[0]
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildWaitPakinSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ barcode: normalizeText(params.barcode),
+ ioStatus:
+ params.ioStatus !== undefined && params.ioStatus !== null && params.ioStatus !== ''
+ ? normalizeNumber(params.ioStatus)
+ : void 0,
+ status:
+ params.status !== undefined && params.status !== null && params.status !== ''
+ ? normalizeNumber(params.status)
+ : void 0,
+ memo: normalizeText(params.memo)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(
+ ([, value]) => value !== '' && value !== void 0 && value !== null
+ )
+ )
+}
+
+export function buildWaitPakinPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWaitPakinSearchParams(params)
+ }
+}
+
+export function normalizeWaitPakinDetailRecord(record = {}) {
+ const statusMeta = getWaitPakinStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ code: normalizeText(record.code) || '--',
+ barcode: normalizeText(record.barcode) || '--',
+ anfme: record.anfme ?? '--',
+ ioStatus: record.ioStatus ?? void 0,
+ ioStatusText: normalizeText(record.ioStatus$ || record.ioStatusText) || '--',
+ flagDefectText:
+ record.flagDefect === 1 || record.flagDefect === '1'
+ ? '鏄�'
+ : record.flagDefect === 0 || record.flagDefect === '0'
+ ? '鍚�'
+ : '--',
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+ memo: normalizeText(record.memo) || '--',
+ createByText: normalizeText(record.createBy$ || record.createByText || '') || '--',
+ createTimeText: normalizeText(record.createTime$ || record.createTime || '') || '--',
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || '') || '--',
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '') || '--'
+ }
+}
+
+export function normalizeWaitPakinListRow(record = {}) {
+ return normalizeWaitPakinDetailRecord(record)
+}
+
+export function normalizeWaitPakinItemRow(record = {}) {
+ const extendFields =
+ record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
+ return {
+ ...record,
+ pakinId: record.pakinId ?? '--',
+ maktx: normalizeText(record.maktx) || '--',
+ matnrId: record.matnrId ?? '--',
+ matnrCode: normalizeText(record.matnrCode) || '--',
+ asnCode: normalizeText(record.asnCode) || '--',
+ trackCode: normalizeText(record.trackCode) || '--',
+ anfme: record.anfme ?? '--',
+ workQty: record.workQty ?? '--',
+ unit: normalizeText(record.unit) || '--',
+ qty: record.qty ?? '--',
+ batch: normalizeText(record.batch) || '--',
+ isptResultText: normalizeText(record.isptResult$ || record.isptResultText) || '--',
+ memo: normalizeText(record.memo) || '--',
+ extendFieldsText: Object.keys(extendFields).length
+ ? Object.entries(extendFields)
+ .map(([key, value]) => `${normalizeText(key)}:${normalizeText(value)}`)
+ .join('锛�')
+ : '--'
+ }
+}
+
+export function buildWaitPakinPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeWaitPakinListRow(record))
+}
+
+export function buildWaitPakinReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = WAIT_PAKIN_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: WAIT_PAKIN_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...WAIT_PAKIN_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function createWaitPakinItemColumns() {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 70, align: 'center' },
+ { prop: 'maktx', label: '鐗╂枡鍚嶇О', minWidth: 180, showOverflowTooltip: true },
+ { prop: 'matnrCode', label: '鐗╂枡缂栫爜', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'asnCode', label: 'ASN鍗曞彿', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'trackCode', label: '璺熻釜鐮�', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'anfme', label: '鏁伴噺', width: 100, align: 'right' },
+ { prop: 'workQty', label: '鎵ц涓暟閲�', width: 110, align: 'right' },
+ { prop: 'qty', label: '宸插畬鎴愭暟閲�', width: 110, align: 'right' },
+ { prop: 'unit', label: '鍗曚綅', width: 90, align: 'center' },
+ { prop: 'batch', label: '鎵规鍙�', minWidth: 130, showOverflowTooltip: true },
+ { prop: 'isptResultText', label: '璐ㄦ缁撴灉', minWidth: 100, showOverflowTooltip: true },
+ { prop: 'extendFieldsText', label: '鎵╁睍瀛楁', minWidth: 220, showOverflowTooltip: true }
+ ]
+}
+
+export function buildWaitPakinMergePayload(rows = [], siteId) {
+ return {
+ waitPakins: rows
+ .map((item) => {
+ if (item?.id === undefined || item?.id === null || item?.id === '') {
+ return null
+ }
+ return {
+ id: Number(item.id)
+ }
+ })
+ .filter(Boolean),
+ siteId: Number(siteId)
+ }
+}
+
+export function createWaitPakinSiteSearchState() {
+ return {
+ name: '',
+ site: '',
+ deviceSite: ''
+ }
+}
+
+export function buildWaitPakinSiteSearchParams(params = {}) {
+ return normalizeObjectEntries({
+ type: 1,
+ name: params.name,
+ site: params.site,
+ deviceSite: params.deviceSite
+ })
+}
+
+export function normalizeWaitPakinSiteRow(record = {}) {
+ return {
+ ...record,
+ name: normalizeText(record.name) || '--',
+ site: normalizeText(record.site) || '--',
+ deviceSite: normalizeText(record.deviceSite) || '--',
+ wcsCode: normalizeText(record.wcsCode) || '--',
+ label: normalizeText(record.label) || '--',
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '') || '--'
+ }
+}
diff --git a/rsf-design/src/views/orders/wait-pakin/waitPakinTable.columns.js b/rsf-design/src/views/orders/wait-pakin/waitPakinTable.columns.js
new file mode 100644
index 0000000..2b278ba
--- /dev/null
+++ b/rsf-design/src/views/orders/wait-pakin/waitPakinTable.columns.js
@@ -0,0 +1,122 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getWaitPakinStatusMeta } from './waitPakinPage.helpers'
+
+export function createWaitPakinTableColumns({
+ handleView,
+ handleDelete,
+ handleGenerateTask,
+ canDelete = true,
+ canGenerateTask = () => true
+} = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: '缁勬墭鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.code || '--'
+ },
+ {
+ prop: 'barcode',
+ label: '瀹瑰櫒鐮�',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.barcode || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '缁勬墭鏁伴噺',
+ width: 110,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'ioStatusText',
+ label: '缁勬墭鐘舵��',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.ioStatusText || '--'
+ },
+ {
+ prop: 'flagDefectText',
+ label: '涓嶈壇鍝�',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.flagDefectText || '--'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const statusMeta = getWaitPakinStatusMeta(row.statusBool ?? row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 180,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) => {
+ const operations = [{ key: 'view', label: '璇︽儏', icon: 'ri:eye-line' }]
+
+ if (handleGenerateTask && canGenerateTask(row)) {
+ operations.push({ key: 'generate', label: '鐢熸垚浠诲姟', icon: 'ri:play-list-add-line' })
+ }
+
+ if (canDelete && handleDelete) {
+ operations.push({
+ key: 'delete',
+ label: '鍒犻櫎',
+ icon: 'ri:delete-bin-5-line',
+ color: 'var(--art-error)'
+ })
+ }
+
+ return h(ArtButtonMore, {
+ list: operations,
+ onClick: (item) => {
+ if (item.key === 'view') handleView?.(row)
+ if (item.key === 'generate') handleGenerateTask?.([row])
+ if (item.key === 'delete') handleDelete?.(row)
+ }
+ })
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/wave-item/index.vue b/rsf-design/src/views/orders/wave-item/index.vue
new file mode 100644
index 0000000..0271f9a
--- /dev/null
+++ b/rsf-design/src/views/orders/wave-item/index.vue
@@ -0,0 +1,202 @@
+<template>
+ <div class="wave-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <WaveItemDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ buildWaveItemPageQueryParams,
+ buildWaveItemPrintRows,
+ buildWaveItemReportMeta,
+ buildWaveItemSearchParams,
+ createWaveItemSearchState,
+ normalizeWaveItemRow
+ } from './waveItemPage.helpers'
+ import { createWaveItemDetailColumns, createWaveItemTableColumns } from './waveItemTable.columns'
+ import { fetchExportWaveItemReport, fetchGetWaveItemDetail, fetchGetWaveItemMany, fetchWaveItemPage } from '@/api/wave-item'
+ import WaveItemDetailDrawer from './modules/wave-item-detail-drawer.vue'
+
+ defineOptions({ name: 'WaveItemOrder' })
+
+ const userStore = useUserStore()
+ const reportTitle = '娉㈡鏄庣粏鎶ヨ〃'
+ const searchForm = ref(createWaveItemSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const activeItemId = ref(null)
+
+ const reportQueryParams = computed(() => buildWaveItemSearchParams(searchForm.value))
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ尝娆″崟鍙�/鐗╂枡缂栫爜/鐗╂枡鍚嶇О' } },
+ { label: '娉㈡鍗曞彿', key: 'waveCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ尝娆″崟鍙�' } },
+ { label: '鍗曟嵁缂栫爜', key: 'orderCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ崟鎹紪鐮�' } },
+ { label: '鐗╂枡缂栫爜', key: 'matnrCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�' } },
+ { label: '鐗╂枡鍚嶇О', key: 'maktx', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�' } },
+ { label: '鎵规', key: 'batch', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ壒娆�' } },
+ { label: '渚涘簲鍟嗘壒娆�', key: 'splrBatch', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规' } },
+ { label: '鍔ㄦ�佸瓧娈电储寮�', key: 'fieldsIndex', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ姩鎬佸瓧娈电储寮�' } },
+ { label: '寮�濮嬫椂闂�', key: 'timeStart', type: 'date', props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' } },
+ { label: '缁撴潫鏃堕棿', key: 'timeEnd', type: 'date', props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' } }
+ ])
+
+ async function openDetail(row) {
+ activeItemId.value = row.id
+ detailDrawerVisible.value = true
+ await loadDetailResource()
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchWaveItemPage,
+ apiParams: buildWaveItemPageQueryParams({
+ ...searchForm.value,
+ pageSize: 20
+ }),
+ columnsFactory: () => createWaveItemTableColumns({ handleActionClick: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeWaveItemRow(item)) : [])
+ }
+ })
+
+ async function loadDetailResource() {
+ if (!activeItemId.value) {
+ return
+ }
+
+ const detailResponse = await guardRequestWithMessage(fetchGetWaveItemDetail(activeItemId.value), {}, { timeoutMessage: '娉㈡鏄庣粏璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' })
+ detailData.value = normalizeWaveItemRow(detailResponse)
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildWaveItemPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createWaveItemSearchState()
+ resetSearchParams()
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetWaveItemMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchWaveItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta: rawPreviewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'wave-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportWaveItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildWaveItemPrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildWaveItemReportMeta({
+ previewMeta: rawPreviewMeta.value,
+ count: previewRows.value.length,
+ orientation: rawPreviewMeta.value?.reportStyle?.orientation || 'landscape'
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/wave-item/modules/wave-item-detail-drawer.vue b/rsf-design/src/views/orders/wave-item/modules/wave-item-detail-drawer.vue
new file mode 100644
index 0000000..a065dcc
--- /dev/null
+++ b/rsf-design/src/views/orders/wave-item/modules/wave-item-detail-drawer.vue
@@ -0,0 +1,55 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="娉㈡鏄庣粏璇︽儏"
+ size="80%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="wave-item-detail-scroll">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="娉㈡鍗曞彿">{{ detail.waveCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁缂栫爜">{{ detail.orderCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍔ㄦ�佸瓧娈电储寮�">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴旈厤鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸查厤鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鏁伴噺">{{ detail.stockQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鐘舵��">
+ <ElTag :type="detail.exceStatusTagType || 'info'" effect="light">
+ {{ detail.exceStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅" :span="4">{{ detail.stockLocsText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'WaveItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
+
+<style scoped>
+ .wave-item-detail-scroll {
+ height: calc(100vh - 120px);
+ }
+</style>
diff --git a/rsf-design/src/views/orders/wave-item/waveItemPage.helpers.js b/rsf-design/src/views/orders/wave-item/waveItemPage.helpers.js
new file mode 100644
index 0000000..a8f0500
--- /dev/null
+++ b/rsf-design/src/views/orders/wave-item/waveItemPage.helpers.js
@@ -0,0 +1,165 @@
+export const WAVE_ITEM_REPORT_TITLE = '娉㈡鏄庣粏鎶ヨ〃'
+export const WAVE_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+const WAVE_ITEM_STATUS_MAP = {
+ 0: { label: '鏈墽琛�', tagType: 'info' },
+ 1: { label: '鎵ц涓�', tagType: 'warning' },
+ 2: { label: '鏆傚仠', tagType: 'warning' },
+ 3: { label: '宸蹭笅鍙�', tagType: 'primary' },
+ 4: { label: '浠诲姟瀹屾垚', tagType: 'success' }
+}
+
+function getItemStatusConfig(status, statusText) {
+ const fallback = WAVE_ITEM_STATUS_MAP[Number(status)] || {
+ label: statusText || '-',
+ tagType: 'info'
+ }
+ return {
+ label: statusText || fallback.label,
+ tagType: fallback.tagType
+ }
+}
+
+function normalizeStockLocs(stockLocs) {
+ if (!stockLocs) {
+ return '[]'
+ }
+ if (typeof stockLocs === 'string') {
+ return stockLocs
+ }
+ try {
+ return JSON.stringify(stockLocs)
+ } catch {
+ return '[]'
+ }
+}
+
+export function createWaveItemSearchState() {
+ return {
+ condition: '',
+ waveId: '',
+ waveCode: '',
+ orderCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ splrBatch: '',
+ unit: '',
+ fieldsIndex: '',
+ status: '',
+ exceStatus: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function buildWaveItemSearchParams(params = {}) {
+ const result = {}
+ ;[
+ 'condition',
+ 'waveCode',
+ 'orderCode',
+ 'matnrCode',
+ 'maktx',
+ 'batch',
+ 'splrBatch',
+ 'unit',
+ 'fieldsIndex',
+ 'timeStart',
+ 'timeEnd'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.waveId !== '' && params.waveId !== undefined && params.waveId !== null) {
+ result.waveId = Number(params.waveId)
+ }
+
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ result.status = Number(params.status)
+ }
+
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = Number(params.exceStatus)
+ }
+
+ return result
+}
+
+export function buildWaveItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWaveItemSearchParams(params)
+ }
+}
+
+export function normalizeWaveItemRow(record = {}) {
+ const statusConfig = getItemStatusConfig(record.exceStatus, record['exceStatus$'])
+ return {
+ ...record,
+ waveCode: record.waveCode || '-',
+ orderCode: record.orderCode || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ batch: record.batch || '-',
+ splrBatch: record.splrBatch || '-',
+ unit: record.unit || '-',
+ fieldsIndex: record.fieldsIndex || '-',
+ anfme: normalizeNumber(record.anfme),
+ workQty: normalizeNumber(record.workQty),
+ qty: normalizeNumber(record.qty),
+ stockQty: normalizeNumber(record.stockQty),
+ stockLocsText: normalizeStockLocs(record.stockLocs),
+ exceStatusText: statusConfig.label,
+ exceStatusTagType: statusConfig.tagType,
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-'
+ }
+}
+
+export function buildWaveItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeWaveItemRow(record))
+}
+
+export function buildWaveItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = WAVE_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: WAVE_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...WAVE_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/orders/wave-item/waveItemTable.columns.js b/rsf-design/src/views/orders/wave-item/waveItemTable.columns.js
new file mode 100644
index 0000000..5851c75
--- /dev/null
+++ b/rsf-design/src/views/orders/wave-item/waveItemTable.columns.js
@@ -0,0 +1,180 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+
+export function createWaveItemTableColumns({ handleActionClick }) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'waveCode',
+ label: '娉㈡鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orderCode',
+ label: '鍗曟嵁缂栫爜',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '搴旈厤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '宸查厤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'stockQty',
+ label: '搴撳瓨鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'fieldsIndex',
+ label: '鍔ㄦ�佸瓧娈电储寮�',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'exceStatusText',
+ label: '鎵ц鐘舵��',
+ width: 120,
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: row.exceStatusTagType || 'info', effect: 'light' },
+ () => row.exceStatusText
+ )
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: [
+ {
+ key: 'view',
+ label: '鏌ョ湅璇︽儏',
+ icon: 'ri:eye-line'
+ }
+ ],
+ onClick: (item) => handleActionClick(item, row)
+ })
+ }
+ ]
+}
+
+export function createWaveItemDetailColumns() {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'waveCode',
+ label: '娉㈡鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orderCode',
+ label: '鍗曟嵁缂栫爜',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '搴旈厤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '宸查厤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'stockQty',
+ label: '搴撳瓨鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'stockLocsText',
+ label: '搴撲綅',
+ minWidth: 220,
+ showOverflowTooltip: true
+ }
+ ]
+}
diff --git a/rsf-design/src/views/orders/wave/index.vue b/rsf-design/src/views/orders/wave/index.vue
new file mode 100644
index 0000000..eaa0a3f
--- /dev/null
+++ b/rsf-design/src/views/orders/wave/index.vue
@@ -0,0 +1,416 @@
+<template>
+ <div class="wave-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <WaveDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ :data="detailTableData"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+
+ <WavePublicTaskDialog
+ v-model:visible="publicTaskDialogVisible"
+ :loading="publicTaskLoading"
+ :submit-loading="publicTaskSubmitting"
+ :wave="publicTaskWave"
+ :data="publicTaskRows"
+ :columns="publicTaskColumns"
+ :pagination="publicTaskPagination"
+ :can-submit="publicTaskCanSubmit"
+ @size-change="handlePublicTaskSizeChange"
+ @current-change="handlePublicTaskCurrentChange"
+ @submit="handleSubmitPublicTask"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ buildWaveDetailQueryParams,
+ buildWavePageQueryParams,
+ buildWavePrintRows,
+ buildWaveReportMeta,
+ buildWaveSearchParams,
+ createWaveSearchState,
+ normalizeWaveItemRow,
+ normalizeWaveRow,
+ WAVE_REPORT_STYLE
+ } from './wavePage.helpers'
+ import {
+ createWaveDetailItemColumns,
+ createWavePreviewItemColumns,
+ createWaveTableColumns
+ } from './waveTable.columns'
+ import {
+ fetchContinueWave,
+ fetchExportWaveReport,
+ fetchGetWaveDetail,
+ fetchGetWaveMany,
+ fetchPauseWave,
+ fetchPublicWaveTask,
+ fetchStopWave,
+ fetchWavePage,
+ fetchWavePreviewPage
+ } from '@/api/wave'
+ import WaveDetailDrawer from './modules/wave-detail-drawer.vue'
+ import WavePublicTaskDialog from './modules/wave-public-task-dialog.vue'
+
+ defineOptions({ name: 'WaveOrder' })
+
+ const userStore = useUserStore()
+ const reportTitle = '娉㈡鍗曟姤琛�'
+ const searchForm = ref(createWaveSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const detailTableData = ref([])
+ const activeWaveId = ref(null)
+ const publicTaskDialogVisible = ref(false)
+ const publicTaskLoading = ref(false)
+ const publicTaskSubmitting = ref(false)
+ const publicTaskWave = ref({})
+ const publicTaskRows = ref([])
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const publicTaskPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildWaveSearchParams(searchForm.value))
+ const detailColumns = computed(() => createWaveDetailItemColumns())
+ const publicTaskColumns = computed(() => createWavePreviewItemColumns())
+ const publicTaskCanSubmit = computed(
+ () => publicTaskRows.value.length > 0 && publicTaskRows.value.every((row) => row.stockLocsText && row.stockLocsText !== '[]')
+ )
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ尝娆″崟鍙�/澶囨敞' } },
+ { label: '娉㈡鍗曞彿', key: 'code', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ尝娆″崟鍙�' } },
+ {
+ label: '娉㈡绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: { clearable: true, options: [{ label: '鎵嬪姩', value: 0 }, { label: '鑷姩', value: 1 }] }
+ },
+ {
+ label: '娉㈡鐘舵��',
+ key: 'exceStatus',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '绛夊緟鎵ц', value: 0 },
+ { label: '姝e湪鎵ц', value: 1 },
+ { label: '鏆傚仠鎵ц', value: 2 },
+ { label: '鎵ц瀹屾垚', value: 3 }
+ ]
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: { clearable: true, options: [{ label: '姝e父', value: 1 }, { label: '绂佺敤', value: 0 }] }
+ },
+ { label: '澶囨敞', key: 'memo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ娉�' } },
+ { label: '寮�濮嬫椂闂�', key: 'timeStart', type: 'date', props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' } },
+ { label: '缁撴潫鏃堕棿', key: 'timeEnd', type: 'date', props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' } }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function openDetail(row) {
+ activeWaveId.value = row.id
+ detailPagination.current = 1
+ detailDrawerVisible.value = true
+ await loadDetailResources()
+ }
+
+ async function openPublicTask(row) {
+ publicTaskWave.value = normalizeWaveRow(row)
+ publicTaskPagination.current = 1
+ publicTaskDialogVisible.value = true
+ await loadPublicTaskResources()
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) return
+ try {
+ if (action.key === 'view') {
+ await openDetail(row)
+ return
+ }
+ if (action.key === 'print') {
+ await handlePrint({ ids: [row.id], pageSize: 1 })
+ return
+ }
+ if (action.key === 'publicTask') {
+ await openPublicTask(row)
+ return
+ }
+ if (action.key === 'pause') {
+ await fetchPauseWave(row.id)
+ ElMessage.success('娉㈡宸叉殏鍋�')
+ await refreshData()
+ return
+ }
+ if (action.key === 'continue') {
+ await fetchContinueWave(row.id)
+ ElMessage.success('娉㈡宸茬户缁�')
+ await refreshData()
+ return
+ }
+ if (action.key === 'stop') {
+ await ElMessageBox.confirm(`纭畾缁堟娉㈡鍗� ${row.code || ''} 鍚楋紵`, '缁堟纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchStopWave(row.id)
+ ElMessage.success('娉㈡宸茬粓姝�')
+ await refreshData()
+ }
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '娉㈡鎿嶄綔澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchWavePage,
+ apiParams: buildWavePageQueryParams(searchForm.value),
+ columnsFactory: () => createWaveTableColumns({ handleActionClick })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeWaveRow(item)) : [])
+ }
+ })
+
+ async function loadDetailResources() {
+ if (!activeWaveId.value) {
+ return
+ }
+
+ detailLoading.value = true
+ try {
+ const [detailResponse, previewResponse] = await Promise.all([
+ guardRequestWithMessage(fetchGetWaveDetail(activeWaveId.value), {}, { timeoutMessage: '娉㈡鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }),
+ guardRequestWithMessage(
+ fetchWavePreviewPage(buildWaveDetailQueryParams({ waveId: activeWaveId.value, current: detailPagination.current, pageSize: detailPagination.size })),
+ { records: [], total: 0, current: detailPagination.current, size: detailPagination.size },
+ { timeoutMessage: '娉㈡棰勮鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ ])
+ detailData.value = normalizeWaveRow(detailResponse)
+ detailTableData.value = Array.isArray(previewResponse?.records) ? previewResponse.records.map((item) => normalizeWaveItemRow(item)) : []
+ updatePaginationState(detailPagination, previewResponse, detailPagination.current, detailPagination.size)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function loadPublicTaskResources() {
+ if (!publicTaskWave.value?.id) {
+ return
+ }
+
+ publicTaskLoading.value = true
+ try {
+ const previewResponse = await guardRequestWithMessage(
+ fetchWavePreviewPage(buildWaveDetailQueryParams({ waveId: publicTaskWave.value.id, current: publicTaskPagination.current, pageSize: publicTaskPagination.size })),
+ { records: [], total: 0, current: publicTaskPagination.current, size: publicTaskPagination.size },
+ { timeoutMessage: '娉㈡涓嬪彂棰勮鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ publicTaskRows.value = Array.isArray(previewResponse?.records) ? previewResponse.records.map((item) => normalizeWaveItemRow(item)) : []
+ updatePaginationState(publicTaskPagination, previewResponse, publicTaskPagination.current, publicTaskPagination.size)
+ } finally {
+ publicTaskLoading.value = false
+ }
+ }
+
+ async function handleSubmitPublicTask() {
+ try {
+ publicTaskSubmitting.value = true
+ const response = await fetchPublicWaveTask({
+ wave: publicTaskWave.value,
+ waveItem: publicTaskRows.value
+ })
+ const result = defaultResponseAdapter(response)
+ if (result?.code !== 200 && result?.success !== true) {
+ throw new Error(result?.message || '娉㈡涓嬪彂澶辫触')
+ }
+ ElMessage.success(result?.message || '娉㈡宸蹭笅鍙�')
+ publicTaskDialogVisible.value = false
+ await refreshData()
+ if (detailDrawerVisible.value && activeWaveId.value === publicTaskWave.value.id) {
+ await loadDetailResources()
+ }
+ } catch (error) {
+ ElMessage.error(error?.message || '娉㈡涓嬪彂澶辫触')
+ } finally {
+ publicTaskSubmitting.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = { ...searchForm.value, ...params }
+ replaceSearchParams(buildWaveSearchParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createWaveSearchState()
+ resetSearchParams()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ loadDetailResources()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailResources()
+ }
+
+ function handlePublicTaskSizeChange(size) {
+ publicTaskPagination.size = size
+ loadPublicTaskResources()
+ }
+
+ function handlePublicTaskCurrentChange(current) {
+ publicTaskPagination.current = current
+ loadPublicTaskResources()
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetWaveMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchWavePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta: rawPreviewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'wave.xlsx',
+ requestExport: (payload) =>
+ fetchExportWaveReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildWavePrintRows(records),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...WAVE_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildWaveReportMeta({
+ previewMeta: rawPreviewMeta.value,
+ count: previewRows.value.length,
+ orientation: rawPreviewMeta.value?.reportStyle?.orientation || WAVE_REPORT_STYLE.orientation
+ })
+ )
+</script>
diff --git a/rsf-design/src/views/orders/wave/modules/wave-detail-drawer.vue b/rsf-design/src/views/orders/wave/modules/wave-detail-drawer.vue
new file mode 100644
index 0000000..3009a49
--- /dev/null
+++ b/rsf-design/src/views/orders/wave/modules/wave-detail-drawer.vue
@@ -0,0 +1,75 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="娉㈡鍗曡鎯�"
+ size="88%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="wave-detail-scroll">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="娉㈡鍗曞彿">{{ detail.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="娉㈡绫诲瀷">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="娉㈡鐘舵��">{{ detail.exceStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="Number(detail.status) === 1 ? 'success' : 'danger'" effect="light">
+ {{ detail.statusLabel || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="搴旂洏鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸茬洏鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曟嵁鏁伴噺">{{ detail.orderNum ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍝佺被鏁伴噺">{{ detail.groupQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣浣嶇疆">{{ detail.targSite || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎸囧畾绔欑偣">{{ detail.stationId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎸囧畾搴撲綅">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElCard shadow="never" class="border border-[var(--art-border-color)]">
+ <template #header>
+ <div class="text-sm font-medium text-[var(--art-text-gray-800)]">娉㈡棰勮鏄庣粏 - 鐗╂枡缂栫爜</div>
+ </template>
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </ElCard>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'WaveDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
+
+<style scoped>
+ .wave-detail-scroll {
+ height: calc(100vh - 120px);
+ }
+</style>
diff --git a/rsf-design/src/views/orders/wave/modules/wave-public-task-dialog.vue b/rsf-design/src/views/orders/wave/modules/wave-public-task-dialog.vue
new file mode 100644
index 0000000..28969a6
--- /dev/null
+++ b/rsf-design/src/views/orders/wave/modules/wave-public-task-dialog.vue
@@ -0,0 +1,68 @@
+<template>
+ <ElDialog
+ :model-value="visible"
+ title="娉㈡涓嬪彂浠诲姟"
+ width="88%"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex max-h-[calc(100vh-240px)] flex-col gap-4 overflow-hidden">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="娉㈡鍗曞彿">{{ wave.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="娉㈡绫诲瀷">{{ wave.typeLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="娉㈡鐘舵��">{{ wave.exceStatusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ wave.workQty ?? '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElAlert
+ v-if="!canSubmit"
+ type="warning"
+ :title="warningText"
+ :closable="false"
+ show-icon
+ />
+
+ <ElCard shadow="never" class="border border-[var(--art-border-color)] flex-1 min-h-0">
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </ElCard>
+ </div>
+
+ <template #footer>
+ <div class="flex items-center justify-end gap-3">
+ <ElButton @click="handleVisibleChange(false)">鍏抽棴</ElButton>
+ <ElButton type="primary" :loading="submitLoading" :disabled="!canSubmit" @click="$emit('submit')">
+ 涓嬪彂浠诲姟
+ </ElButton>
+ </div>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ defineOptions({ name: 'WavePublicTaskDialog' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ submitLoading: { type: Boolean, default: false },
+ wave: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) },
+ canSubmit: { type: Boolean, default: false },
+ warningText: { type: String, default: '娉㈡棰勮鏁版嵁涓嶅彲鐢紝璇峰厛妫�鏌ュ簱浣嶉厤缃�' }
+ })
+
+ const emit = defineEmits(['update:visible', 'size-change', 'current-change', 'submit'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/orders/wave/wavePage.helpers.js b/rsf-design/src/views/orders/wave/wavePage.helpers.js
new file mode 100644
index 0000000..1e3c9f6
--- /dev/null
+++ b/rsf-design/src/views/orders/wave/wavePage.helpers.js
@@ -0,0 +1,312 @@
+export const WAVE_REPORT_TITLE = '娉㈡鍗曟姤琛�'
+export const WAVE_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+const WAVE_STATUS_MAP = {
+ 0: { label: '绛夊緟鎵ц', tagType: 'info' },
+ 1: { label: '姝e湪鎵ц', tagType: 'warning' },
+ 2: { label: '鏆傚仠鎵ц', tagType: 'warning' },
+ 3: { label: '鎵ц瀹屾垚', tagType: 'success' }
+}
+
+const WAVE_ITEM_STATUS_MAP = {
+ 0: { label: '鏈墽琛�', tagType: 'info' },
+ 1: { label: '鎵ц涓�', tagType: 'warning' },
+ 2: { label: '鏆傚仠', tagType: 'warning' },
+ 3: { label: '宸蹭笅鍙�', tagType: 'primary' },
+ 4: { label: '浠诲姟瀹屾垚', tagType: 'success' }
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+function getStatusConfig(status, statusText) {
+ const fallback = WAVE_STATUS_MAP[Number(status)] || {
+ label: statusText || '-',
+ tagType: 'info'
+ }
+ return {
+ label: statusText || fallback.label,
+ tagType: fallback.tagType
+ }
+}
+
+function getItemStatusConfig(status, statusText) {
+ const fallback = WAVE_ITEM_STATUS_MAP[Number(status)] || {
+ label: statusText || '-',
+ tagType: 'info'
+ }
+ return {
+ label: statusText || fallback.label,
+ tagType: fallback.tagType
+ }
+}
+
+function normalizeStockLocs(stockLocs) {
+ if (!stockLocs) {
+ return '[]'
+ }
+ if (typeof stockLocs === 'string') {
+ return stockLocs
+ }
+ try {
+ return JSON.stringify(stockLocs)
+ } catch {
+ return '[]'
+ }
+}
+
+export function createWaveSearchState() {
+ return {
+ condition: '',
+ code: '',
+ type: '',
+ exceStatus: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function buildWaveSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'code', 'memo', 'timeStart', 'timeEnd'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.type !== '' && params.type !== undefined && params.type !== null) {
+ result.type = Number(params.type)
+ }
+
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = Number(params.exceStatus)
+ }
+
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ result.status = Number(params.status)
+ }
+
+ return result
+}
+
+export function buildWavePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWaveSearchParams(params)
+ }
+}
+
+export function buildWaveDetailQueryParams(params = {}) {
+ return {
+ waveId: params.waveId,
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20
+ }
+}
+
+export function normalizeWaveRow(record = {}) {
+ const statusConfig = getStatusConfig(record.exceStatus, record['exceStatus$'])
+ const progress = normalizeNumber(record.anfme) > 0
+ ? Math.min(Math.round((normalizeNumber(record.workQty) / normalizeNumber(record.anfme)) * 100), 100)
+ : 0
+
+ return {
+ ...record,
+ code: record.code || '-',
+ typeLabel: record['type$'] || record.type || '-',
+ exceStatusText: statusConfig.label,
+ exceStatusTagType: statusConfig.tagType,
+ statusLabel: record['status$'] || record.status || '-',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ workQty: normalizeNumber(record.workQty),
+ orderNum: normalizeNumber(record.orderNum),
+ groupQty: normalizeNumber(record.groupQty),
+ progress,
+ createTimeText: record['createTime$'] || record.createTime || '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createByText: record['createBy$'] || '-',
+ updateByText: record['updateBy$'] || '-',
+ memo: record.memo || '-',
+ targSite: record.targSite || '-',
+ stationId: record.stationId || '-',
+ locCode: record.locCode || '-',
+ canPublicTask: Number(record.exceStatus) === 0,
+ canPause: Number(record.exceStatus) === 1,
+ canContinue: Number(record.exceStatus) === 2,
+ canStop: Number(record.exceStatus) !== 3
+ }
+}
+
+export function normalizeWaveItemRow(record = {}) {
+ const statusConfig = getItemStatusConfig(record.exceStatus, record['exceStatus$'])
+ return {
+ ...record,
+ waveCode: record.waveCode || '-',
+ orderCode: record.orderCode || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ batch: record.batch || '-',
+ splrBatch: record.splrBatch || '-',
+ unit: record.unit || '-',
+ trackCode: record.trackCode || '-',
+ fieldsIndex: record.fieldsIndex || '-',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ workQty: normalizeNumber(record.workQty),
+ stockQty: normalizeNumber(record.stockQty),
+ stockLocsText: normalizeStockLocs(record.stockLocs),
+ statusLabel: record['status$'] || record.status || '-',
+ exceStatusText: statusConfig.label,
+ exceStatusTagType: statusConfig.tagType,
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-'
+ }
+}
+
+export function buildWavePrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeWaveRow(record))
+}
+
+export function buildWaveReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = WAVE_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: WAVE_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...WAVE_REPORT_STYLE,
+ orientation
+ }
+ }
+}
+
+export function getWaveActionList(row = {}) {
+ const normalizedRow = normalizeWaveRow(row)
+ return [
+ {
+ key: 'view',
+ label: '鏌ョ湅璇︽儏',
+ icon: 'ri:eye-line'
+ },
+ {
+ key: 'publicTask',
+ label: '涓嬪彂浠诲姟',
+ icon: 'ri:play-circle-line',
+ disabled: !normalizedRow.canPublicTask
+ },
+ {
+ key: 'pause',
+ label: '鏆傚仠',
+ icon: 'ri:pause-circle-line',
+ disabled: !normalizedRow.canPause
+ },
+ {
+ key: 'continue',
+ label: '缁х画',
+ icon: 'ri:play-line',
+ disabled: !normalizedRow.canContinue
+ },
+ {
+ key: 'stop',
+ label: '缁堟',
+ icon: 'ri:stop-circle-line',
+ disabled: !normalizedRow.canStop
+ },
+ {
+ key: 'print',
+ label: '鎵撳嵃',
+ icon: 'ri:printer-line'
+ }
+ ]
+}
+
+export function createWaveItemSearchState() {
+ return {
+ condition: '',
+ waveId: '',
+ waveCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ splrBatch: '',
+ orderCode: '',
+ unit: '',
+ fieldsIndex: '',
+ status: '',
+ exceStatus: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function buildWaveItemSearchParams(params = {}) {
+ const result = {}
+ ;[
+ 'condition',
+ 'waveCode',
+ 'matnrCode',
+ 'maktx',
+ 'batch',
+ 'splrBatch',
+ 'orderCode',
+ 'unit',
+ 'fieldsIndex',
+ 'timeStart',
+ 'timeEnd'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.waveId !== '' && params.waveId !== undefined && params.waveId !== null) {
+ result.waveId = Number(params.waveId)
+ }
+
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ result.status = Number(params.status)
+ }
+
+ if (params.exceStatus !== '' && params.exceStatus !== undefined && params.exceStatus !== null) {
+ result.exceStatus = Number(params.exceStatus)
+ }
+
+ return result
+}
+
+export function buildWaveItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWaveItemSearchParams(params)
+ }
+}
diff --git a/rsf-design/src/views/orders/wave/waveTable.columns.js b/rsf-design/src/views/orders/wave/waveTable.columns.js
new file mode 100644
index 0000000..9b0041a
--- /dev/null
+++ b/rsf-design/src/views/orders/wave/waveTable.columns.js
@@ -0,0 +1,179 @@
+import { h } from 'vue'
+import { ElProgress, ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getWaveActionList } from './wavePage.helpers'
+
+export function createWaveTableColumns({ handleActionClick }) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'code',
+ label: '娉㈡鍗曞彿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'typeLabel',
+ label: '娉㈡绫诲瀷',
+ width: 110
+ },
+ {
+ prop: 'exceStatusText',
+ label: '娉㈡鐘舵��',
+ width: 120,
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: row.exceStatusTagType || 'info', effect: 'light' },
+ () => row.exceStatusText
+ )
+ },
+ {
+ prop: 'anfme',
+ label: '搴旂洏鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '宸茬洏鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'orderNum',
+ label: '鍗曟嵁鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'progress',
+ label: '杩涘害',
+ width: 160,
+ formatter: (row) =>
+ h(ElProgress, {
+ percentage: Number(row.progress || 0),
+ strokeWidth: 10,
+ status: row.progress >= 100 ? 'success' : undefined,
+ striped: false,
+ showText: true
+ })
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'statusLabel',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: Number(row.status) === 1 ? 'success' : 'danger', effect: 'light' },
+ () => row.statusLabel
+ )
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 220,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: getWaveActionList(row),
+ onClick: (item) => handleActionClick(item, row)
+ })
+ }
+ ]
+}
+
+export function createWavePreviewItemColumns() {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'waveCode',
+ label: '娉㈡鍙�',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'orderCode',
+ label: '鍗曟嵁缂栫爜',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 130,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '搴旈厤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '宸查厤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'stockQty',
+ label: '搴撳瓨鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'stockLocsText',
+ label: '搴撲綅',
+ minWidth: 220,
+ showOverflowTooltip: true
+ }
+ ]
+}
+
+export function createWaveDetailItemColumns() {
+ return createWavePreviewItemColumns()
+}
diff --git a/rsf-design/src/views/reports/statistic-count/index.vue b/rsf-design/src/views/reports/statistic-count/index.vue
new file mode 100644
index 0000000..74d3070
--- /dev/null
+++ b/rsf-design/src/views/reports/statistic-count/index.vue
@@ -0,0 +1,167 @@
+<template>
+ <div class="statistic-count-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchStatisticCountPage } from '@/api/statistic-count'
+ import {
+ buildStatisticCountPageQueryParams,
+ createStatisticCountSearchState,
+ normalizeStatisticCountRow
+ } from './statisticCountPage.helpers'
+ import { createStatisticCountTableColumns } from './statisticCountTable.columns'
+
+ defineOptions({ name: 'StatisticCount' })
+
+ const loading = ref(false)
+ const tableData = ref([])
+ const searchForm = ref(createStatisticCountSearchState())
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ粺璁℃棩鏈�/鐗╂枡缂栫爜'
+ }
+ },
+ {
+ label: '缁熻鏃ユ湡',
+ key: 'dayTime',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨缁熻鏃ユ湡'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ }
+ ])
+
+ const { columns, columnChecks } = useTableColumns(() => createStatisticCountTableColumns())
+
+ function updatePaginationState(response) {
+ pagination.total = Number(response?.total || 0)
+ pagination.current = Number(response?.current || pagination.current || 1)
+ pagination.size = Number(response?.size || pagination.size || 20)
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchStatisticCountPage(
+ buildStatisticCountPageQueryParams({
+ ...searchForm.value,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: pagination.current,
+ size: pagination.size
+ },
+ {
+ timeoutMessage: '鏃ュ嚭鍏ュ簱姹囨�荤粺璁″姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
+
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeStatisticCountRow(record))
+ : []
+ updatePaginationState(response)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ searchForm.value = createStatisticCountSearchState()
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ onMounted(() => {
+ loadPageData()
+ })
+</script>
diff --git a/rsf-design/src/views/reports/statistic-count/statisticCountPage.helpers.js b/rsf-design/src/views/reports/statistic-count/statisticCountPage.helpers.js
new file mode 100644
index 0000000..26039f0
--- /dev/null
+++ b/rsf-design/src/views/reports/statistic-count/statisticCountPage.helpers.js
@@ -0,0 +1,55 @@
+export const STATISTIC_COUNT_REPORT_TITLE = '鏃ュ嚭鍏ュ簱姹囨�荤粺璁�'
+
+export function createStatisticCountSearchState() {
+ return {
+ condition: '',
+ dayTime: '',
+ matnrCode: '',
+ maktx: '',
+ batch: ''
+ }
+}
+
+export function buildStatisticCountPageQueryParams(params = {}) {
+ const values = {
+ condition: params.condition,
+ dayTime: params.dayTime,
+ matnrCode: params.matnrCode,
+ maktx: params.maktx,
+ batch: params.batch
+ }
+
+ return Object.entries(values).reduce(
+ (result, [key, value]) => {
+ if (value === undefined || value === null) {
+ return result
+ }
+ if (typeof value === 'string') {
+ const normalized = value.trim()
+ if (!normalized) {
+ return result
+ }
+ result[key] = normalized
+ return result
+ }
+ result[key] = value
+ return result
+ },
+ {
+ current: Number(params.current) || 1,
+ pageSize: Number(params.pageSize || params.size) || 20
+ }
+ )
+}
+
+export function normalizeStatisticCountRow(row = {}) {
+ return {
+ ...row,
+ count: Number(row.count || 0),
+ anfme: Number(row.anfme || 0),
+ inAnfmeCount: Number(row.inAnfmeCount || 0),
+ outAnfmeCount: Number(row.outAnfmeCount || 0),
+ inAnfme: Number(row.inAnfme || 0),
+ outAnfme: Number(row.outAnfme || 0)
+ }
+}
diff --git a/rsf-design/src/views/reports/statistic-count/statisticCountTable.columns.js b/rsf-design/src/views/reports/statistic-count/statisticCountTable.columns.js
new file mode 100644
index 0000000..40f9372
--- /dev/null
+++ b/rsf-design/src/views/reports/statistic-count/statisticCountTable.columns.js
@@ -0,0 +1,44 @@
+export function createStatisticCountTableColumns() {
+ return [
+ {
+ prop: 'id',
+ label: 'ID',
+ minWidth: 80
+ },
+ {
+ prop: 'dayTime',
+ label: '缁熻鏃ユ湡',
+ minWidth: 120
+ },
+ {
+ prop: 'count',
+ label: '璁板綍鏁�',
+ minWidth: 100
+ },
+ {
+ prop: 'inAnfmeCount',
+ label: '鍏ュ簱绗旀暟',
+ minWidth: 110
+ },
+ {
+ prop: 'outAnfmeCount',
+ label: '鍑哄簱绗旀暟',
+ minWidth: 110
+ },
+ {
+ prop: 'anfme',
+ label: '鎬绘暟閲�',
+ minWidth: 110
+ },
+ {
+ prop: 'inAnfme',
+ label: '鍏ュ簱鏁伴噺',
+ minWidth: 110
+ },
+ {
+ prop: 'outAnfme',
+ label: '鍑哄簱鏁伴噺',
+ minWidth: 110
+ }
+ ]
+}
diff --git a/rsf-design/src/views/statistics/in-statistic-item/inStatisticItemPage.helpers.js b/rsf-design/src/views/statistics/in-statistic-item/inStatisticItemPage.helpers.js
new file mode 100644
index 0000000..c8f7fce
--- /dev/null
+++ b/rsf-design/src/views/statistics/in-statistic-item/inStatisticItemPage.helpers.js
@@ -0,0 +1,86 @@
+import { getInStatisticTaskStatusMeta, getInStatisticTaskTypeMeta } from '../in-statistic/inStatisticPage.helpers.js'
+
+export const IN_STATISTIC_ITEM_PAGE_TITLE = '鍏ュ簱缁熻鏄庣粏'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+export function createInStatisticItemSearchState() {
+ return {
+ condition: '',
+ dayTime: '',
+ locCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: ''
+ }
+}
+
+export function getInStatisticItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildInStatisticItemSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ dayTime: normalizeText(params.dayTime),
+ locCode: normalizeText(params.locCode),
+ matnrCode: normalizeText(params.matnrCode),
+ maktx: normalizeText(params.maktx),
+ batch: normalizeText(params.batch),
+ taskType: normalizeNumber(params.taskType, 1),
+ taskStatus: normalizeNumber(params.taskStatus, 100)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildInStatisticItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildInStatisticItemSearchParams(params)
+ }
+}
+
+export function normalizeInStatisticItemRow(record = {}) {
+ const taskTypeMeta = getInStatisticTaskTypeMeta(record.taskType ?? record.task_type)
+ const taskStatusMeta = getInStatisticTaskStatusMeta(record.taskStatus ?? record.task_status)
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ dayTimeText: normalizeText(record.dayTime || record.day_time || ''),
+ taskTypeText: normalizeText(record.taskTypeText || record['taskType$'] || taskTypeMeta.text),
+ taskTypeTagType: normalizeText(record.taskTypeTagType || taskTypeMeta.type) || 'info',
+ taskStatusText: normalizeText(record.taskStatusText || record['taskStatus$'] || taskStatusMeta.text),
+ taskStatusTagType: normalizeText(record.taskStatusTagType || taskStatusMeta.type) || 'info',
+ locCode: normalizeText(record.locCode || record.loc_code || ''),
+ barcode: normalizeText(record.barcode || ''),
+ matnrCode: normalizeText(record.matnrCode || record.matnr_code || ''),
+ maktx: normalizeText(record.maktx || ''),
+ batch: normalizeText(record.batch || ''),
+ unit: normalizeText(record.unit || ''),
+ anfme: record.anfme ?? '--',
+ fieldsIndex: normalizeText(record.fieldsIndex || record.fields_index || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || record.createBy || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || record.updateBy || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ memo: normalizeText(record.memo || '')
+ }
+}
diff --git a/rsf-design/src/views/statistics/in-statistic-item/inStatisticItemTable.columns.js b/rsf-design/src/views/statistics/in-statistic-item/inStatisticItemTable.columns.js
new file mode 100644
index 0000000..4a3cba9
--- /dev/null
+++ b/rsf-design/src/views/statistics/in-statistic-item/inStatisticItemTable.columns.js
@@ -0,0 +1,128 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createInStatisticItemTableColumns({ handleView } = {}) {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'id',
+ label: 'ID',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.id ?? '--'
+ },
+ {
+ prop: 'dayTimeText',
+ label: '缁熻鏃ユ湡',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.dayTimeText || '--'
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locCode || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'barcode',
+ label: '鎵樼洏鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.barcode || '--'
+ },
+ {
+ prop: 'taskTypeText',
+ label: '浠诲姟绫诲瀷',
+ width: 110,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.taskTypeTagType || 'info', effect: 'light' }, () => row?.taskTypeText || '--')
+ },
+ {
+ prop: 'taskStatusText',
+ label: '浠诲姟鐘舵��',
+ width: 140,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.taskStatusTagType || 'info', effect: 'light' }, () => row?.taskStatusText || '--')
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/statistics/in-statistic-item/index.vue b/rsf-design/src/views/statistics/in-statistic-item/index.vue
new file mode 100644
index 0000000..923c631
--- /dev/null
+++ b/rsf-design/src/views/statistics/in-statistic-item/index.vue
@@ -0,0 +1,86 @@
+<template>
+ <div class="in-statistic-item-page art-full-height">
+ <ArtSearchBar v-model="searchForm" :items="searchItems" :showExpand="true" @search="handleSearch" @reset="handleReset" />
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+ <InStatisticItemDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchInStatisticItemPage } from '@/api/in-statistic-item'
+ import {
+ buildInStatisticItemPageQueryParams,
+ buildInStatisticItemSearchParams,
+ createInStatisticItemSearchState,
+ getInStatisticItemPaginationKey,
+ normalizeInStatisticItemRow
+ } from './inStatisticItemPage.helpers'
+ import { createInStatisticItemTableColumns } from './inStatisticItemTable.columns'
+ import InStatisticItemDetailDrawer from './modules/in-statistic-item-detail-drawer.vue'
+
+ defineOptions({ name: 'InStatisticItem' })
+
+ const searchForm = ref(createInStatisticItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�/缂栫爜/鎵规/搴撲綅' } },
+ { label: '缁熻鏃ユ湡', key: 'dayTime', type: 'date', props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' } },
+ { label: '搴撲綅', key: 'locCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ簱浣�' } },
+ { label: '鐗╂枡缂栫爜', key: 'matnrCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�' } },
+ { label: '鐗╂枡鍚嶇О', key: 'maktx', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�' } },
+ { label: '鎵规', key: 'batch', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ壒娆�' } }
+ ])
+
+ function openDetail(row) {
+ detailData.value = normalizeInStatisticItemRow(row)
+ detailDrawerVisible.value = true
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchInStatisticItemPage,
+ apiParams: buildInStatisticItemPageQueryParams(searchForm.value),
+ paginationKey: getInStatisticItemPaginationKey(),
+ columnsFactory: () => createInStatisticItemTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeInStatisticItemRow(item)) : [])
+ }
+ })
+
+ function handleSearch(params) {
+ replaceSearchParams(buildInStatisticItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createInStatisticItemSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/statistics/in-statistic-item/modules/in-statistic-item-detail-drawer.vue b/rsf-design/src/views/statistics/in-statistic-item/modules/in-statistic-item-detail-drawer.vue
new file mode 100644
index 0000000..85845bf
--- /dev/null
+++ b/rsf-design/src/views/statistics/in-statistic-item/modules/in-statistic-item-detail-drawer.vue
@@ -0,0 +1,53 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ :title="IN_STATISTIC_ITEM_PAGE_TITLE + '璇︽儏'"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="ID">{{ detail.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁熻鏃ユ湡">{{ detail.dayTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟绫诲瀷">
+ <ElTag :type="detail.taskTypeTagType || 'info'" effect="light">{{ detail.taskTypeText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鐘舵��">
+ <ElTag :type="detail.taskStatusTagType || 'info'" effect="light">{{ detail.taskStatusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { IN_STATISTIC_ITEM_PAGE_TITLE } from '../inStatisticItemPage.helpers'
+
+ defineOptions({ name: 'InStatisticItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/statistics/in-statistic/inStatisticPage.helpers.js b/rsf-design/src/views/statistics/in-statistic/inStatisticPage.helpers.js
new file mode 100644
index 0000000..65b0c6c
--- /dev/null
+++ b/rsf-design/src/views/statistics/in-statistic/inStatisticPage.helpers.js
@@ -0,0 +1,107 @@
+const TASK_TYPE_META = {
+ 1: { text: '鍏ュ簱', type: 'success' }
+}
+
+const TASK_STATUS_META = {
+ 98: { text: '鍏ュ簱瀹屾垚', type: 'success' },
+ 99: { text: '涓婃姤瀹屾垚', type: 'warning' },
+ 100: { text: '搴撳瓨鏇存柊瀹屾垚', type: 'success' },
+ 101: { text: '鍒涘缓鍑哄簱浠诲姟', type: 'info' }
+}
+
+export const IN_STATISTIC_PAGE_TITLE = '鍏ュ簱缁熻'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+export function createInStatisticSearchState() {
+ return {
+ condition: '',
+ dayTime: '',
+ maktx: '',
+ matnrCode: '',
+ batch: ''
+ }
+}
+
+export function getInStatisticPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildInStatisticSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ dayTime: normalizeText(params.dayTime),
+ maktx: normalizeText(params.maktx),
+ matnrCode: normalizeText(params.matnrCode),
+ batch: normalizeText(params.batch),
+ taskType: normalizeNumber(params.taskType, 1),
+ taskStatus: normalizeNumber(params.taskStatus, 100)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildInStatisticPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildInStatisticSearchParams(params)
+ }
+}
+
+export function getInStatisticTaskTypeMeta(taskType) {
+ if (taskType === null || taskType === undefined || taskType === '') {
+ return { text: '鍏ュ簱', type: 'success' }
+ }
+ return TASK_TYPE_META[Number(taskType)] || { text: String(taskType), type: 'info' }
+}
+
+export function getInStatisticTaskStatusMeta(taskStatus) {
+ if (taskStatus === null || taskStatus === undefined || taskStatus === '') {
+ return { text: '-', type: 'info' }
+ }
+ return TASK_STATUS_META[Number(taskStatus)] || { text: String(taskStatus), type: 'info' }
+}
+
+export function normalizeInStatisticRow(record = {}) {
+ const taskTypeMeta = getInStatisticTaskTypeMeta(record.taskType ?? record.task_type)
+ const taskStatusMeta = getInStatisticTaskStatusMeta(record.taskStatus ?? record.task_status)
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ dayTimeText: normalizeText(record.dayTime || record.day_time || ''),
+ taskTypeText: normalizeText(record.taskTypeText || record['taskType$'] || taskTypeMeta.text),
+ taskTypeTagType: normalizeText(record.taskTypeTagType || taskTypeMeta.type) || 'info',
+ taskStatusText: normalizeText(record.taskStatusText || record['taskStatus$'] || taskStatusMeta.text),
+ taskStatusTagType: normalizeText(record.taskStatusTagType || taskStatusMeta.type) || 'info',
+ locCode: normalizeText(record.locCode || record.loc_code || ''),
+ barcode: normalizeText(record.barcode || ''),
+ matnrCode: normalizeText(record.matnrCode || record.matnr_code || ''),
+ maktx: normalizeText(record.maktx || ''),
+ batch: normalizeText(record.batch || ''),
+ unit: normalizeText(record.unit || ''),
+ anfme: record.anfme ?? '--',
+ fieldsIndex: normalizeText(record.fieldsIndex || record.fields_index || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || record.createBy || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || record.updateBy || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ memo: normalizeText(record.memo || '')
+ }
+}
diff --git a/rsf-design/src/views/statistics/in-statistic/inStatisticTable.columns.js b/rsf-design/src/views/statistics/in-statistic/inStatisticTable.columns.js
new file mode 100644
index 0000000..e0a11a3
--- /dev/null
+++ b/rsf-design/src/views/statistics/in-statistic/inStatisticTable.columns.js
@@ -0,0 +1,86 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createInStatisticTableColumns({ handleView } = {}) {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'id',
+ label: 'ID',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.id ?? '--'
+ },
+ {
+ prop: 'dayTimeText',
+ label: '缁熻鏃ユ湡',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.dayTimeText || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'taskTypeText',
+ label: '浠诲姟绫诲瀷',
+ width: 110,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.taskTypeTagType || 'info', effect: 'light' }, () => row?.taskTypeText || '--')
+ },
+ {
+ prop: 'taskStatusText',
+ label: '浠诲姟鐘舵��',
+ width: 140,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.taskStatusTagType || 'info', effect: 'light' }, () => row?.taskStatusText || '--')
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/statistics/in-statistic/index.vue b/rsf-design/src/views/statistics/in-statistic/index.vue
new file mode 100644
index 0000000..b72dfa9
--- /dev/null
+++ b/rsf-design/src/views/statistics/in-statistic/index.vue
@@ -0,0 +1,91 @@
+<template>
+ <div class="in-statistic-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+ <InStatisticDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchInStatisticPage } from '@/api/in-statistic'
+ import {
+ buildInStatisticPageQueryParams,
+ buildInStatisticSearchParams,
+ createInStatisticSearchState,
+ getInStatisticPaginationKey,
+ normalizeInStatisticRow
+ } from './inStatisticPage.helpers'
+ import { createInStatisticTableColumns } from './inStatisticTable.columns'
+ import InStatisticDetailDrawer from './modules/in-statistic-detail-drawer.vue'
+
+ defineOptions({ name: 'InStatistic' })
+
+ const searchForm = ref(createInStatisticSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�/缂栫爜/鎵规' } },
+ { label: '缁熻鏃ユ湡', key: 'dayTime', type: 'date', props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' } },
+ { label: '鐗╂枡鍚嶇О', key: 'maktx', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�' } },
+ { label: '鐗╂枡缂栫爜', key: 'matnrCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�' } },
+ { label: '鎵规', key: 'batch', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ壒娆�' } }
+ ])
+
+ function openDetail(row) {
+ detailData.value = normalizeInStatisticRow(row)
+ detailDrawerVisible.value = true
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchInStatisticPage,
+ apiParams: buildInStatisticPageQueryParams(searchForm.value),
+ paginationKey: getInStatisticPaginationKey(),
+ columnsFactory: () => createInStatisticTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeInStatisticRow(item)) : [])
+ }
+ })
+
+ function handleSearch(params) {
+ replaceSearchParams(buildInStatisticSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createInStatisticSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/statistics/in-statistic/modules/in-statistic-detail-drawer.vue b/rsf-design/src/views/statistics/in-statistic/modules/in-statistic-detail-drawer.vue
new file mode 100644
index 0000000..2c11312
--- /dev/null
+++ b/rsf-design/src/views/statistics/in-statistic/modules/in-statistic-detail-drawer.vue
@@ -0,0 +1,48 @@
+<template>
+ <ElDrawer :model-value="visible" :title="IN_STATISTIC_PAGE_TITLE + '璇︽儏'" size="72%" @update:model-value="handleVisibleChange">
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="ID">{{ detail.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁熻鏃ユ湡">{{ detail.dayTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟绫诲瀷">
+ <ElTag :type="detail.taskTypeTagType || 'info'" effect="light">{{ detail.taskTypeText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鐘舵��">
+ <ElTag :type="detail.taskStatusTagType || 'info'" effect="light">{{ detail.taskStatusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { IN_STATISTIC_PAGE_TITLE } from '../inStatisticPage.helpers'
+
+ defineOptions({ name: 'InStatisticDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/statistics/out-statistic-item/index.vue b/rsf-design/src/views/statistics/out-statistic-item/index.vue
new file mode 100644
index 0000000..9d0f0a4
--- /dev/null
+++ b/rsf-design/src/views/statistics/out-statistic-item/index.vue
@@ -0,0 +1,148 @@
+<template>
+ <div class="out-statistic-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <OutStatisticItemDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchOutStatisticItemPage } from '@/api/out-statistic-item'
+ import {
+ buildOutStatisticItemPageQueryParams,
+ buildOutStatisticItemSearchParams,
+ createOutStatisticItemSearchState,
+ getOutStatisticItemPaginationKey,
+ normalizeOutStatisticItemRow
+ } from './outStatisticItemPage.helpers'
+ import { createOutStatisticItemTableColumns } from './outStatisticItemTable.columns'
+ import OutStatisticItemDetailDrawer from './modules/out-statistic-item-detail-drawer.vue'
+
+ defineOptions({ name: 'OutStatisticItem' })
+
+ const searchForm = ref(createOutStatisticItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�/缂栫爜/鎵规/搴撲綅'
+ }
+ },
+ {
+ label: '缁熻鏃ユ湡',
+ key: 'dayTime',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD'
+ }
+ },
+ {
+ label: '搴撲綅',
+ key: 'locCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailData.value = normalizeOutStatisticItemRow(row)
+ detailDrawerVisible.value = true
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchOutStatisticItemPage,
+ apiParams: buildOutStatisticItemPageQueryParams(searchForm.value),
+ paginationKey: getOutStatisticItemPaginationKey(),
+ columnsFactory: () =>
+ createOutStatisticItemTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeOutStatisticItemRow(item)) : []
+ }
+ })
+
+ function handleSearch(params) {
+ replaceSearchParams(buildOutStatisticItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createOutStatisticItemSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/statistics/out-statistic-item/modules/out-statistic-item-detail-drawer.vue b/rsf-design/src/views/statistics/out-statistic-item/modules/out-statistic-item-detail-drawer.vue
new file mode 100644
index 0000000..3a55fd5
--- /dev/null
+++ b/rsf-design/src/views/statistics/out-statistic-item/modules/out-statistic-item-detail-drawer.vue
@@ -0,0 +1,57 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ :title="OUT_STATISTIC_ITEM_PAGE_TITLE + '璇︽儏'"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="ID">{{ detail.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁熻鏃ユ湡">{{ detail.dayTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟绫诲瀷">
+ <ElTag :type="detail.taskTypeTagType || 'info'" effect="light">
+ {{ detail.taskTypeText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鐘舵��">
+ <ElTag :type="detail.taskStatusTagType || 'info'" effect="light">
+ {{ detail.taskStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { OUT_STATISTIC_ITEM_PAGE_TITLE } from '../outStatisticItemPage.helpers'
+
+ defineOptions({ name: 'OutStatisticItemDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/statistics/out-statistic-item/outStatisticItemPage.helpers.js b/rsf-design/src/views/statistics/out-statistic-item/outStatisticItemPage.helpers.js
new file mode 100644
index 0000000..3d1266f
--- /dev/null
+++ b/rsf-design/src/views/statistics/out-statistic-item/outStatisticItemPage.helpers.js
@@ -0,0 +1,89 @@
+import {
+ getOutStatisticTaskStatusMeta,
+ getOutStatisticTaskTypeMeta
+} from '../out-statistic/outStatisticPage.helpers.js'
+
+export const OUT_STATISTIC_ITEM_PAGE_TITLE = '鍑哄簱缁熻鏄庣粏'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+export function createOutStatisticItemSearchState() {
+ return {
+ condition: '',
+ dayTime: '',
+ locCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: ''
+ }
+}
+
+export function getOutStatisticItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildOutStatisticItemSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ dayTime: normalizeText(params.dayTime),
+ locCode: normalizeText(params.locCode),
+ matnrCode: normalizeText(params.matnrCode),
+ maktx: normalizeText(params.maktx),
+ batch: normalizeText(params.batch),
+ taskType: normalizeNumber(params.taskType, 101),
+ taskStatus: normalizeNumber(params.taskStatus, 200)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildOutStatisticItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildOutStatisticItemSearchParams(params)
+ }
+}
+
+export function normalizeOutStatisticItemRow(record = {}) {
+ const taskTypeMeta = getOutStatisticTaskTypeMeta(record.taskType ?? record.task_type)
+ const taskStatusMeta = getOutStatisticTaskStatusMeta(record.taskStatus ?? record.task_status)
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ dayTimeText: normalizeText(record.dayTime || record.day_time || ''),
+ taskTypeText: normalizeText(record.taskTypeText || record['taskType$'] || taskTypeMeta.text),
+ taskTypeTagType: normalizeText(record.taskTypeTagType || taskTypeMeta.type) || 'info',
+ taskStatusText: normalizeText(record.taskStatusText || record['taskStatus$'] || taskStatusMeta.text),
+ taskStatusTagType: normalizeText(record.taskStatusTagType || taskStatusMeta.type) || 'info',
+ locCode: normalizeText(record.locCode || record.loc_code || ''),
+ barcode: normalizeText(record.barcode || ''),
+ matnrCode: normalizeText(record.matnrCode || record.matnr_code || ''),
+ maktx: normalizeText(record.maktx || ''),
+ batch: normalizeText(record.batch || ''),
+ unit: normalizeText(record.unit || ''),
+ anfme: record.anfme ?? '--',
+ fieldsIndex: normalizeText(record.fieldsIndex || record.fields_index || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || record.createBy || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || record.updateBy || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ memo: normalizeText(record.memo || '')
+ }
+}
diff --git a/rsf-design/src/views/statistics/out-statistic-item/outStatisticItemTable.columns.js b/rsf-design/src/views/statistics/out-statistic-item/outStatisticItemTable.columns.js
new file mode 100644
index 0000000..3fc2c1b
--- /dev/null
+++ b/rsf-design/src/views/statistics/out-statistic-item/outStatisticItemTable.columns.js
@@ -0,0 +1,133 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createOutStatisticItemTableColumns({ handleView } = {}) {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'id',
+ label: 'ID',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.id ?? '--'
+ },
+ {
+ prop: 'dayTimeText',
+ label: '缁熻鏃ユ湡',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.dayTimeText || '--'
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locCode || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'barcode',
+ label: '鎵樼洏鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.barcode || '--'
+ },
+ {
+ prop: 'taskTypeText',
+ label: '浠诲姟绫诲瀷',
+ width: 110,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.taskTypeTagType || 'info', effect: 'light' }, () => row?.taskTypeText || '--')
+ },
+ {
+ prop: 'taskStatusText',
+ label: '浠诲姟鐘舵��',
+ width: 140,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.taskStatusTagType || 'info', effect: 'light' }, () => row?.taskStatusText || '--')
+ },
+ {
+ prop: 'createByText',
+ label: '鍒涘缓浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createByText || '--'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.createTimeText || '--'
+ },
+ {
+ prop: 'updateByText',
+ label: '鏇存柊浜�',
+ minWidth: 120,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateByText || '--'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/statistics/out-statistic/index.vue b/rsf-design/src/views/statistics/out-statistic/index.vue
new file mode 100644
index 0000000..d75320b
--- /dev/null
+++ b/rsf-design/src/views/statistics/out-statistic/index.vue
@@ -0,0 +1,140 @@
+<template>
+ <div class="out-statistic-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <OutStatisticDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchOutStatisticPage } from '@/api/out-statistic'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ buildOutStatisticPageQueryParams,
+ buildOutStatisticSearchParams,
+ createOutStatisticSearchState,
+ getOutStatisticPaginationKey,
+ normalizeOutStatisticRow,
+ OUT_STATISTIC_PAGE_TITLE
+ } from './outStatisticPage.helpers'
+ import { createOutStatisticTableColumns } from './outStatisticTable.columns'
+ import OutStatisticDetailDrawer from './modules/out-statistic-detail-drawer.vue'
+
+ defineOptions({ name: 'OutStatistic' })
+
+ const searchForm = ref(createOutStatisticSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�/缂栫爜/鎵规'
+ }
+ },
+ {
+ label: '缁熻鏃ユ湡',
+ key: 'dayTime',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailData.value = normalizeOutStatisticRow(row)
+ detailDrawerVisible.value = true
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchOutStatisticPage,
+ apiParams: buildOutStatisticPageQueryParams(searchForm.value),
+ paginationKey: getOutStatisticPaginationKey(),
+ columnsFactory: () =>
+ createOutStatisticTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeOutStatisticRow(item)) : [])
+ }
+ })
+
+ function handleSearch(params) {
+ replaceSearchParams(buildOutStatisticSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createOutStatisticSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/statistics/out-statistic/modules/out-statistic-detail-drawer.vue b/rsf-design/src/views/statistics/out-statistic/modules/out-statistic-detail-drawer.vue
new file mode 100644
index 0000000..5a79746
--- /dev/null
+++ b/rsf-design/src/views/statistics/out-statistic/modules/out-statistic-detail-drawer.vue
@@ -0,0 +1,57 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ :title="OUT_STATISTIC_PAGE_TITLE + '璇︽儏'"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="ID">{{ detail.id ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁熻鏃ユ湡">{{ detail.dayTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟绫诲瀷">
+ <ElTag :type="detail.taskTypeTagType || 'info'" effect="light">
+ {{ detail.taskTypeText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鐘舵��">
+ <ElTag :type="detail.taskStatusTagType || 'info'" effect="light">
+ {{ detail.taskStatusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撲綅">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵樼洏鐮�">{{ detail.barcode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绱㈠紩">{{ detail.fieldsIndex || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ detail.createByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { OUT_STATISTIC_PAGE_TITLE } from '../outStatisticPage.helpers'
+
+ defineOptions({ name: 'OutStatisticDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/statistics/out-statistic/outStatisticPage.helpers.js b/rsf-design/src/views/statistics/out-statistic/outStatisticPage.helpers.js
new file mode 100644
index 0000000..9ad8a84
--- /dev/null
+++ b/rsf-design/src/views/statistics/out-statistic/outStatisticPage.helpers.js
@@ -0,0 +1,116 @@
+const TASK_TYPE_META = {
+ 101: { text: '鍑哄簱', type: 'warning' }
+}
+
+const TASK_STATUS_META = {
+ 196: { text: '绛夊緟纭', type: 'info' },
+ 197: { text: '绛夊緟瀹瑰櫒鍒拌揪', type: 'info' },
+ 198: { text: '鍑哄簱瀹屾垚', type: 'success' },
+ 199: { text: '鎾涓�/鐩樼偣涓�/寰呯‘璁�', type: 'warning' },
+ 200: { text: '搴撳瓨鏇存柊瀹屾垚', type: 'success' }
+}
+
+export const OUT_STATISTIC_PAGE_TITLE = '鍑哄簱缁熻'
+export const OUT_STATISTIC_REPORT_TITLE = '鍑哄簱缁熻鎶ヨ〃'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : fallback
+}
+
+export function createOutStatisticSearchState() {
+ return {
+ condition: '',
+ dayTime: '',
+ maktx: '',
+ matnrCode: '',
+ batch: ''
+ }
+}
+
+export function getOutStatisticPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildOutStatisticSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ dayTime: normalizeText(params.dayTime),
+ maktx: normalizeText(params.maktx),
+ matnrCode: normalizeText(params.matnrCode),
+ batch: normalizeText(params.batch),
+ taskType: normalizeNumber(params.taskType, 101),
+ taskStatus: normalizeNumber(params.taskStatus, 200)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildOutStatisticPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildOutStatisticSearchParams(params)
+ }
+}
+
+export function getOutStatisticTaskTypeMeta(taskType) {
+ if (taskType === null || taskType === undefined || taskType === '') {
+ return { text: '鍑哄簱', type: 'warning' }
+ }
+ return TASK_TYPE_META[Number(taskType)] || { text: String(taskType), type: 'info' }
+}
+
+export function getOutStatisticTaskStatusMeta(taskStatus) {
+ if (taskStatus === null || taskStatus === undefined || taskStatus === '') {
+ return { text: '-', type: 'info' }
+ }
+ return TASK_STATUS_META[Number(taskStatus)] || { text: String(taskStatus), type: 'info' }
+}
+
+export function normalizeOutStatisticRow(record = {}) {
+ const taskTypeMeta = getOutStatisticTaskTypeMeta(record.taskType ?? record.task_type)
+ const taskStatusMeta = getOutStatisticTaskStatusMeta(record.taskStatus ?? record.task_status)
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ dayTimeText: normalizeText(record.dayTime || record.day_time || ''),
+ taskTypeText: normalizeText(record.taskTypeText || record['taskType$'] || taskTypeMeta.text),
+ taskTypeTagType: normalizeText(record.taskTypeTagType || taskTypeMeta.type) || 'info',
+ taskStatusText: normalizeText(record.taskStatusText || record['taskStatus$'] || taskStatusMeta.text),
+ taskStatusTagType: normalizeText(record.taskStatusTagType || taskStatusMeta.type) || 'info',
+ locCode: normalizeText(record.locCode || record.loc_code || ''),
+ barcode: normalizeText(record.barcode || ''),
+ matnrCode: normalizeText(record.matnrCode || record.matnr_code || ''),
+ maktx: normalizeText(record.maktx || ''),
+ batch: normalizeText(record.batch || ''),
+ unit: normalizeText(record.unit || ''),
+ anfme: record.anfme ?? '--',
+ fieldsIndex: normalizeText(record.fieldsIndex || record.fields_index || ''),
+ createByText: normalizeText(record.createBy$ || record.createByText || record.createBy || ''),
+ createTimeText: normalizeText(record.createTime$ || record.createTime || ''),
+ updateByText: normalizeText(record.updateBy$ || record.updateByText || record.updateBy || ''),
+ updateTimeText: normalizeText(record.updateTime$ || record.updateTime || ''),
+ memo: normalizeText(record.memo || '')
+ }
+}
+
+export function buildOutStatisticPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeOutStatisticRow(record))
+}
diff --git a/rsf-design/src/views/statistics/out-statistic/outStatisticTable.columns.js b/rsf-design/src/views/statistics/out-statistic/outStatisticTable.columns.js
new file mode 100644
index 0000000..991fd12
--- /dev/null
+++ b/rsf-design/src/views/statistics/out-statistic/outStatisticTable.columns.js
@@ -0,0 +1,91 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createOutStatisticTableColumns({ handleView } = {}) {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'id',
+ label: 'ID',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.id ?? '--'
+ },
+ {
+ prop: 'dayTimeText',
+ label: '缁熻鏃ユ湡',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.dayTimeText || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 120,
+ align: 'right',
+ formatter: (row) => row.anfme ?? '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90,
+ align: 'center',
+ formatter: (row) => row.unit || '--'
+ },
+ {
+ prop: 'taskTypeText',
+ label: '浠诲姟绫诲瀷',
+ width: 110,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.taskTypeTagType || 'info', effect: 'light' }, () => row?.taskTypeText || '--')
+ },
+ {
+ prop: 'taskStatusText',
+ label: '浠诲姟鐘舵��',
+ width: 140,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.taskStatusTagType || 'info', effect: 'light' }, () => row?.taskStatusText || '--')
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/stock/stock-transfer/index.vue b/rsf-design/src/views/stock/stock-transfer/index.vue
new file mode 100644
index 0000000..31d88dc
--- /dev/null
+++ b/rsf-design/src/views/stock/stock-transfer/index.vue
@@ -0,0 +1,312 @@
+<template>
+ <div class="stock-transfer-page art-full-height">
+ <ElCard class="art-table-card mb-4">
+ <template #header>
+ <div class="flex items-center justify-between gap-3">
+ <div class="text-sm font-medium text-[var(--art-text-gray-800)]">搴撲綅杞Щ浠诲姟</div>
+ <div class="text-xs text-[var(--art-text-gray-500)]">婧愬簱浣嶇紪鐮佽揪鍒� 7 浣嶅悗鑷姩鑱旀兂鐩爣搴撲綅骞跺姞杞芥槑缁�</div>
+ </div>
+ </template>
+
+ <div class="flex flex-col gap-4 xl:flex-row xl:items-end">
+ <div class="min-w-0 flex-1">
+ <div class="mb-2 text-sm font-medium text-[var(--art-text-gray-700)]">婧愬簱浣�</div>
+ <ElInput
+ v-model="orgLoc"
+ clearable
+ placeholder="璇疯緭鍏ユ簮搴撲綅缂栫爜"
+ @keyup.enter="handleLoadSourceData"
+ />
+ </div>
+
+ <div class="min-w-0 flex-1">
+ <div class="mb-2 text-sm font-medium text-[var(--art-text-gray-700)]">鐩爣搴撲綅</div>
+ <ElAutocomplete
+ v-model="tarLoc"
+ :fetch-suggestions="queryTargetLoc"
+ :trigger-on-focus="true"
+ :loading="targetLocLoading"
+ clearable
+ placeholder="璇疯緭鍏ョ洰鏍囧簱浣嶇紪鐮�"
+ />
+ </div>
+
+ <div class="flex flex-wrap gap-2">
+ <ElButton :loading="loading" @click="handleLoadSourceData">鍔犺浇鏄庣粏</ElButton>
+ <ElButton type="primary" :disabled="!canGenerateTask" @click="handleGenerateTask">
+ 鐢熸垚绉诲簱浠诲姟
+ </ElButton>
+ </div>
+ </div>
+ </ElCard>
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadSourceData">
+ <template #left>
+ <div class="text-sm text-[var(--art-text-gray-500)]">
+ 婧愬簱浣嶏細{{ orgLoc || '--' }}锛岀洰鏍囧簱浣嶏細{{ tarLoc || '--' }}
+ </div>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <StockTransferDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref, watch } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchStockTransferEnabledFields,
+ fetchStockTransferMoveTask,
+ fetchStockTransferSourceDetail,
+ fetchStockTransferSourcePage,
+ fetchStockTransferTargetLocPage
+ } from '@/api/stock-transfer'
+ import StockTransferDetailDrawer from './modules/stock-transfer-detail-drawer.vue'
+ import { createStockTransferTableColumns } from './stockTransferTable.columns'
+ import {
+ buildStockTransferSourcePageQueryParams,
+ buildStockTransferTargetLocPageQueryParams,
+ buildStockTransferTaskPayload,
+ normalizeStockTransferDetail,
+ normalizeStockTransferEnabledFields,
+ normalizeStockTransferRow,
+ normalizeStockTransferTargetLocOptions
+ } from './stockTransferPage.helpers'
+
+ defineOptions({ name: 'StockTransfer' })
+
+ const enabledFields = ref([])
+ const orgLoc = ref('')
+ const tarLoc = ref('')
+ const memo = ref('')
+ const loading = ref(false)
+ const targetLocLoading = ref(false)
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const tableData = ref([])
+
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const canGenerateTask = computed(() => {
+ const sourceLoc = String(orgLoc.value || '').trim()
+ const targetLoc = String(tarLoc.value || '').trim()
+ return sourceLoc.length >= 7 && targetLoc.length > 0 && !loading.value
+ })
+
+ const { columns, columnChecks, resetColumns } = useTableColumns(() =>
+ createStockTransferTableColumns({
+ enabledFields: enabledFields.value,
+ handleView: openDetail
+ })
+ )
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ function normalizeOrgLocValue(value) {
+ return String(value ?? '').trim()
+ }
+
+ function resetTableState() {
+ tableData.value = []
+ pagination.current = 1
+ pagination.total = 0
+ }
+
+ async function loadEnabledFields() {
+ const fields = await guardRequestWithMessage(fetchStockTransferEnabledFields(), [], {
+ timeoutMessage: '鎵╁睍瀛楁鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ enabledFields.value = normalizeStockTransferEnabledFields(fields)
+ resetColumns()
+ if (normalizeOrgLocValue(orgLoc.value).length >= 7) {
+ await loadSourceData()
+ }
+ }
+
+ async function loadSourceData() {
+ const sourceLoc = normalizeOrgLocValue(orgLoc.value)
+ if (sourceLoc.length < 7) {
+ resetTableState()
+ return
+ }
+
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchStockTransferSourcePage(
+ buildStockTransferSourcePageQueryParams({
+ orgLoc: sourceLoc,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: pagination.current,
+ size: pagination.size
+ },
+ { timeoutMessage: '绉诲簱婧愬簱浣嶆槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�' }
+ )
+
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeStockTransferRow(record, enabledFields.value))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ function handleLoadSourceData() {
+ if (normalizeOrgLocValue(orgLoc.value).length < 7) {
+ ElMessage.warning('璇疯緭鍏ユ湁鏁堢殑婧愬簱浣�')
+ resetTableState()
+ return
+ }
+ pagination.current = 1
+ loadSourceData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadSourceData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadSourceData()
+ }
+
+ async function queryTargetLoc(queryString, callback) {
+ const sourceLoc = normalizeOrgLocValue(orgLoc.value)
+ if (sourceLoc.length < 7) {
+ callback([])
+ return
+ }
+
+ targetLocLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchStockTransferTargetLocPage(
+ buildStockTransferTargetLocPageQueryParams({
+ orgLoc: sourceLoc,
+ q: queryString,
+ current: 1,
+ pageSize: 50
+ })
+ ),
+ { records: [], total: 0 },
+ { timeoutMessage: '鐩爣搴撲綅鑱旀兂鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ callback(normalizeStockTransferTargetLocOptions(defaultResponseAdapter(response).records))
+ } catch (error) {
+ callback([])
+ ElMessage.error(error?.message || '鐩爣搴撲綅鑱旀兂鍔犺浇澶辫触')
+ } finally {
+ targetLocLoading.value = false
+ }
+ }
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ detailData.value = normalizeStockTransferRow(row, enabledFields.value)
+ try {
+ const detail = await guardRequestWithMessage(fetchStockTransferSourceDetail(row.id), {}, {
+ timeoutMessage: '绉诲簱婧愬簱浣嶆槑缁嗚鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ detailData.value = normalizeStockTransferDetail(detail, enabledFields.value)
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇鏄庣粏璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function handleGenerateTask() {
+ const sourceLoc = normalizeOrgLocValue(orgLoc.value)
+ const targetLoc = normalizeOrgLocValue(tarLoc.value)
+
+ if (sourceLoc.length < 7) {
+ ElMessage.warning('璇疯緭鍏ユ湁鏁堢殑婧愬簱浣�')
+ return
+ }
+ if (!targetLoc) {
+ ElMessage.warning('璇疯緭鍏ョ洰鏍囧簱浣�')
+ return
+ }
+
+ try {
+ await ElMessageBox.confirm(`纭畾浠� ${sourceLoc} 杞Щ鍒� ${targetLoc} 鍚楋紵`, '鐢熸垚绉诲簱浠诲姟', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchStockTransferMoveTask(
+ buildStockTransferTaskPayload({
+ orgLoc: sourceLoc,
+ tarLoc: targetLoc,
+ memo: memo.value
+ })
+ )
+ ElMessage.success('绉诲簱浠诲姟鐢熸垚鎴愬姛')
+ await loadSourceData()
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') {
+ return
+ }
+ ElMessage.error(error?.message || '绉诲簱浠诲姟鐢熸垚澶辫触')
+ }
+ }
+
+ const scheduleLoadSourceData = useDebounceFn(() => {
+ pagination.current = 1
+ loadSourceData()
+ }, 300)
+
+ watch(orgLoc, (value) => {
+ tarLoc.value = ''
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ const normalized = normalizeOrgLocValue(value)
+ if (normalized.length < 7) {
+ resetTableState()
+ return
+ }
+ scheduleLoadSourceData()
+ })
+
+ onMounted(async () => {
+ await loadEnabledFields()
+ })
+</script>
diff --git a/rsf-design/src/views/stock/stock-transfer/modules/stock-transfer-detail-drawer.vue b/rsf-design/src/views/stock/stock-transfer/modules/stock-transfer-detail-drawer.vue
new file mode 100644
index 0000000..7e6ff67
--- /dev/null
+++ b/rsf-design/src/views/stock/stock-transfer/modules/stock-transfer-detail-drawer.vue
@@ -0,0 +1,78 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撲綅杞Щ鏄庣粏璇︽儏"
+ size="86%"
+ destroy-on-close
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)] pr-1">
+ <div v-if="loading" class="py-6">
+ <ElSkeleton :rows="10" animated />
+ </div>
+ <div v-else class="flex flex-col gap-4">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="甯愰潰搴撳瓨">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勬牸">{{ detail.spec || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍨嬪彿">{{ detail.model || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElCard v-if="dynamicFieldEntries.length > 0" shadow="never" class="border border-[var(--art-border-color)]">
+ <template #header>
+ <div class="text-sm font-medium text-[var(--art-text-gray-800)]">鎵╁睍瀛楁</div>
+ </template>
+ <div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
+ <div
+ v-for="item in dynamicFieldEntries"
+ :key="item.key"
+ class="rounded-lg border border-[var(--art-border-color)] px-3 py-2"
+ >
+ <div class="text-xs text-[var(--art-text-gray-500)]">{{ item.label }}</div>
+ <div class="mt-1 text-sm text-[var(--art-text-gray-800)]">{{ item.value || '--' }}</div>
+ </div>
+ </div>
+ </ElCard>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ defineOptions({ name: 'StockTransferDetailDrawer' })
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const dynamicFieldEntries = computed(() => {
+ const extendFields = props.detail?.extendFields && typeof props.detail.extendFields === 'object'
+ ? props.detail.extendFields
+ : {}
+ return Object.entries(extendFields)
+ .map(([key, value]) => ({
+ key,
+ label: key,
+ value: typeof value === 'string' ? value.trim() : String(value ?? '')
+ }))
+ .filter((item) => item.value !== '')
+ })
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/stock/stock-transfer/stockTransferPage.helpers.js b/rsf-design/src/views/stock/stock-transfer/stockTransferPage.helpers.js
new file mode 100644
index 0000000..b7c3697
--- /dev/null
+++ b/rsf-design/src/views/stock/stock-transfer/stockTransferPage.helpers.js
@@ -0,0 +1,136 @@
+export const STOCK_TRANSFER_DYNAMIC_FIELD_PREFIX = 'extendField__'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+export function createStockTransferSearchState() {
+ return {
+ orgLoc: '',
+ tarLoc: '',
+ memo: ''
+ }
+}
+
+export function buildStockTransferSourcePageQueryParams(params = {}) {
+ const orgLoc = normalizeText(params.orgLoc || params.locCode)
+ const result = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ orderBy: 'create_time desc'
+ }
+ if (orgLoc) {
+ result.locCode = orgLoc
+ }
+ return result
+}
+
+export function buildStockTransferTargetLocPageQueryParams(params = {}) {
+ const result = {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 50
+ }
+ const orgLoc = normalizeText(params.orgLoc || params.locCode)
+ const q = normalizeText(params.q)
+ if (orgLoc) {
+ result.locCode = orgLoc
+ }
+ if (q) {
+ result.q = q
+ }
+ return result
+}
+
+export function buildStockTransferTaskPayload(params = {}) {
+ return {
+ orgLoc: normalizeText(params.orgLoc),
+ tarLoc: normalizeText(params.tarLoc),
+ memo: normalizeText(params.memo)
+ }
+}
+
+export function getStockTransferDynamicFieldKey(fieldName) {
+ return `${STOCK_TRANSFER_DYNAMIC_FIELD_PREFIX}${fieldName}`
+}
+
+export function normalizeStockTransferEnabledFields(fields = []) {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+ return fields
+ .map((item) => ({
+ fields: normalizeText(item.fields),
+ fieldsAlise: normalizeText(item.fieldsAlise || item.fieldsAlias || item.fields)
+ }))
+ .filter((item) => item.fields)
+}
+
+export function normalizeStockTransferTargetLocOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records
+ .map((item) => {
+ if (item === null || item === undefined) {
+ return null
+ }
+ const value = normalizeText(typeof item === 'string' ? item : item.code || item.value || item.id)
+ if (!value) {
+ return null
+ }
+ return {
+ value,
+ label: normalizeText(typeof item === 'string' ? item : item.label || item.code || item.value || value)
+ }
+ })
+ .filter(Boolean)
+}
+
+export function attachStockTransferDynamicFields(record = {}, enabledFields = []) {
+ const extendFields = record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
+ const dynamicValues = {}
+ enabledFields.forEach((field) => {
+ dynamicValues[getStockTransferDynamicFieldKey(field.fields)] = extendFields[field.fields] || ''
+ })
+ return {
+ ...record,
+ extendFields,
+ ...dynamicValues
+ }
+}
+
+export function normalizeStockTransferRow(record = {}, enabledFields = []) {
+ return attachStockTransferDynamicFields(
+ {
+ ...record,
+ locCode: normalizeText(record.locCode) || '-',
+ matnrCode: normalizeText(record.matnrCode) || '-',
+ maktx: normalizeText(record.maktx) || '-',
+ batch: normalizeText(record.batch) || '-',
+ spec: normalizeText(record.spec) || '-',
+ model: normalizeText(record.model) || '-',
+ unit: normalizeText(record.unit) || '-',
+ barcode: normalizeText(record.barcode) || '-',
+ statusText: normalizeText(record['status$'] || record.statusText || '姝e父') || '姝e父',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '-',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '-',
+ anfme: normalizeNumber(record.anfme),
+ workQty: normalizeNumber(record.workQty),
+ qty: normalizeNumber(record.qty),
+ memo: normalizeText(record.memo) || '-'
+ },
+ enabledFields
+ )
+}
+
+export function normalizeStockTransferDetail(record = {}, enabledFields = []) {
+ return normalizeStockTransferRow(record, enabledFields)
+}
diff --git a/rsf-design/src/views/stock/stock-transfer/stockTransferTable.columns.js b/rsf-design/src/views/stock/stock-transfer/stockTransferTable.columns.js
new file mode 100644
index 0000000..134b7dd
--- /dev/null
+++ b/rsf-design/src/views/stock/stock-transfer/stockTransferTable.columns.js
@@ -0,0 +1,83 @@
+import { h } from 'vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createStockTransferTableColumns({ enabledFields = [], handleView } = {}) {
+ const dynamicColumns = enabledFields.map((field) => ({
+ prop: `extendField__${field.fields}`,
+ label: field.fieldsAlise,
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row[`extendField__${field.fields}`] || '-'
+ }))
+
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'anfme',
+ label: '甯愰潰搴撳瓨',
+ width: 120,
+ formatter: (row) => row.anfme ?? 0
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100,
+ formatter: (row) => row.unit || '-'
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => row.statusText || '-'
+ },
+ ...dynamicColumns,
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 100,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ text: '璇︽儏',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/stock/warehouse-areas-item/index.vue b/rsf-design/src/views/stock/warehouse-areas-item/index.vue
new file mode 100644
index 0000000..ae8bdd3
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-areas-item/index.vue
@@ -0,0 +1,482 @@
+<template>
+ <div class="warehouse-areas-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData">
+ <template #left>
+ <ElSpace wrap>
+ <span v-auth="'list'" class="inline-flex">
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </span>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <WarehouseIsptResultDrawer
+ v-model:visible="isptDrawerVisible"
+ :loading="isptLoading"
+ :summary="activeRow"
+ :data="isptTableData"
+ :columns="isptColumns"
+ :pagination="isptPagination"
+ @size-change="handleIsptSizeChange"
+ @current-change="handleIsptCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import {
+ fetchEnabledFields,
+ fetchExportWarehouseAreasItemReport,
+ fetchGetWarehouseAreasItemMany,
+ fetchWarehouseAreasItemIsptPage,
+ fetchWarehouseAreasItemPage
+ } from '@/api/warehouse-areas-item'
+ import WarehouseIsptResultDrawer from './modules/warehouse-ispt-result-drawer.vue'
+ import { createWarehouseAreasItemTableColumns } from './warehouseAreasItemTable.columns'
+ import {
+ buildWarehouseAreasItemIsptQueryParams,
+ buildWarehouseAreasItemPageQueryParams,
+ buildWarehouseAreasItemPrintRows,
+ buildWarehouseAreasItemReportMeta,
+ buildWarehouseAreasItemSearchParams,
+ createWarehouseAreasItemSearchState,
+ getWarehouseAreasItemDynamicFieldKey,
+ normalizeWarehouseAreasItemEnabledFields,
+ normalizeWarehouseAreasItemIsptRow,
+ normalizeWarehouseAreasItemRow,
+ WAREHOUSE_AREAS_ITEM_REPORT_STYLE,
+ WAREHOUSE_AREAS_ITEM_REPORT_TITLE
+ } from './warehouseAreasItemPage.helpers'
+
+ defineOptions({ name: 'WarehouseAreasItem' })
+
+ const userStore = useUserStore()
+ const reportTitle = WAREHOUSE_AREAS_ITEM_REPORT_TITLE
+ const loading = ref(false)
+ const tableData = ref([])
+ const enabledFields = ref([])
+ const selectedRows = ref([])
+ const searchForm = ref(createWarehouseAreasItemSearchState())
+
+ const isptDrawerVisible = ref(false)
+ const isptLoading = ref(false)
+ const isptTableData = ref([])
+ const activeRow = ref({})
+
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const isptPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const reportQueryParams = computed(() => buildWarehouseAreasItemSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鍒掕窡韪彿/鐗╂枡缂栫爜'
+ }
+ },
+ {
+ label: '璁″垝璺熻釜鍙�',
+ key: 'asnCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鍒掕窡韪彿'
+ }
+ },
+ {
+ label: '搴撳尯鍚嶇О',
+ key: 'areaName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱鍖哄悕绉�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鏉$爜',
+ key: 'barcode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ潯鐮�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ },
+ {
+ label: '骞冲彴鍗曞彿',
+ key: 'platOrderCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ钩鍙板崟鍙�'
+ }
+ },
+ {
+ label: '骞冲彴宸ュ崟',
+ key: 'platWorkCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ钩鍙板伐鍗�'
+ }
+ },
+ {
+ label: '椤圭洰缂栫爜',
+ key: 'projectCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ」鐩紪鐮�'
+ }
+ },
+ ...enabledFields.value.map((field) => ({
+ label: field.fieldsAlise,
+ key: getWarehouseAreasItemDynamicFieldKey(field.fields),
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: `璇疯緭鍏�${field.fieldsAlise}`
+ }
+ }))
+ ])
+
+ const { columns, columnChecks, resetColumns } = useTableColumns(() =>
+ createWarehouseAreasItemTableColumns({
+ enabledFields: enabledFields.value,
+ handleViewIspt: openIsptDrawer
+ })
+ )
+
+ const isptColumns = computed(() => [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'ispectId',
+ label: '璐ㄦ鍗旾D',
+ minWidth: 120
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'label',
+ label: '鏍囩',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'dlyQty',
+ label: '閫佹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'anfme',
+ label: '纭鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'splrName',
+ label: '渚涘簲鍟�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'isptResultText',
+ label: '璐ㄦ缁撴灉',
+ minWidth: 120,
+ showOverflowTooltip: true
+ }
+ ])
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadEnabledFieldDefinitions() {
+ const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
+ timeoutMessage: '鎵╁睍瀛楁鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ enabledFields.value = normalizeWarehouseAreasItemEnabledFields(fields)
+ enabledFields.value.forEach((field) => {
+ const dynamicKey = getWarehouseAreasItemDynamicFieldKey(field.fields)
+ if (searchForm.value[dynamicKey] === undefined) {
+ searchForm.value[dynamicKey] = ''
+ }
+ })
+ resetColumns()
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchWarehouseAreasItemPage(
+ buildWarehouseAreasItemPageQueryParams({
+ ...searchForm.value,
+ current: pagination.current,
+ pageSize: pagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: pagination.current,
+ size: pagination.size
+ },
+ {
+ timeoutMessage: '鏀惰揣搴撳瓨鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeWarehouseAreasItemRow(record, enabledFields.value))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function loadIsptData() {
+ if (!activeRow.value?.id) {
+ return
+ }
+
+ isptLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchWarehouseAreasItemIsptPage(
+ buildWarehouseAreasItemIsptQueryParams({
+ id: activeRow.value.id,
+ current: isptPagination.current,
+ pageSize: isptPagination.size
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: isptPagination.current,
+ size: isptPagination.size
+ },
+ {
+ timeoutMessage: '璐ㄦ缁撴灉鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ isptTableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeWarehouseAreasItemIsptRow(record))
+ : []
+ updatePaginationState(isptPagination, response, isptPagination.current, isptPagination.size)
+ } finally {
+ isptLoading.value = false
+ }
+ }
+
+ function openIsptDrawer(row) {
+ activeRow.value = row
+ isptPagination.current = 1
+ isptDrawerVisible.value = true
+ loadIsptData()
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ searchForm.value = createWarehouseAreasItemSearchState()
+ enabledFields.value.forEach((field) => {
+ searchForm.value[getWarehouseAreasItemDynamicFieldKey(field.fields)] = ''
+ })
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ function handleIsptSizeChange(size) {
+ isptPagination.size = size
+ isptPagination.current = 1
+ loadIsptData()
+ }
+
+ function handleIsptCurrentChange(current) {
+ isptPagination.current = current
+ loadIsptData()
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'warehouse-areas-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportWarehouseAreasItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords: async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetWarehouseAreasItemMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchWarehouseAreasItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ },
+ buildPreviewRows: (records) => buildWarehouseAreasItemPrintRows(records, enabledFields.value),
+ buildPreviewMeta: (rows) => {
+ const now = new Date()
+ return {
+ reportTitle,
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: {
+ ...WAREHOUSE_AREAS_ITEM_REPORT_STYLE
+ }
+ }
+ }
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildWarehouseAreasItemReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || WAREHOUSE_AREAS_ITEM_REPORT_STYLE.orientation
+ })
+ )
+
+ onMounted(async () => {
+ await loadEnabledFieldDefinitions()
+ await loadPageData()
+ })
+</script>
diff --git a/rsf-design/src/views/stock/warehouse-areas-item/modules/warehouse-ispt-result-drawer.vue b/rsf-design/src/views/stock/warehouse-areas-item/modules/warehouse-ispt-result-drawer.vue
new file mode 100644
index 0000000..f908310
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-areas-item/modules/warehouse-ispt-result-drawer.vue
@@ -0,0 +1,44 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="璐ㄦ缁撴灉"
+ size="72%"
+ destroy-on-close
+ @update:model-value="emit('update:visible', $event)"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="璁″垝璺熻釜鍙�">{{ summary.asnCode || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳尯鍚嶇О">{{ summary.areaName || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ summary.matnrCode || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ summary.maktx || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ summary.splrBatch || '-' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="褰撳墠璐ㄦ缁撴灉">{{ summary.isptResultText || '-' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="emit('size-change', $event)"
+ @pagination:current-change="emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'WarehouseIsptResultDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ summary: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'size-change', 'current-change'])
+</script>
diff --git a/rsf-design/src/views/stock/warehouse-areas-item/warehouseAreasItemPage.helpers.js b/rsf-design/src/views/stock/warehouse-areas-item/warehouseAreasItemPage.helpers.js
new file mode 100644
index 0000000..e5b715b
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-areas-item/warehouseAreasItemPage.helpers.js
@@ -0,0 +1,201 @@
+export const WAREHOUSE_AREAS_ITEM_REPORT_TITLE = '鏀惰揣搴撳瓨鎶ヨ〃'
+export const WAREHOUSE_AREAS_ITEM_DYNAMIC_FIELD_PREFIX = 'extendField__'
+export const WAREHOUSE_AREAS_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+export function createWarehouseAreasItemSearchState() {
+ return {
+ condition: '',
+ asnCode: '',
+ areaName: '',
+ matnrCode: '',
+ maktx: '',
+ barcode: '',
+ batch: '',
+ platOrderCode: '',
+ platWorkCode: '',
+ projectCode: '',
+ status: void 0
+ }
+}
+
+export function getWarehouseAreasItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getWarehouseAreasItemDynamicFieldKey(fieldName) {
+ return `${WAREHOUSE_AREAS_ITEM_DYNAMIC_FIELD_PREFIX}${fieldName}`
+}
+
+export function normalizeWarehouseAreasItemEnabledFields(fields = []) {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+ return fields
+ .map((item) => ({
+ fields: normalizeText(item.fields),
+ fieldsAlise: normalizeText(item.fieldsAlise || item.fieldsAlias || item.fields)
+ }))
+ .filter((item) => item.fields)
+}
+
+export function buildWarehouseAreasItemSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'asnCode',
+ 'areaName',
+ 'matnrCode',
+ 'maktx',
+ 'barcode',
+ 'batch',
+ 'platOrderCode',
+ 'platWorkCode',
+ 'projectCode'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.status !== undefined && params.status !== null && params.status !== '') {
+ result.status = params.status
+ }
+
+ Object.entries(params).forEach(([key, value]) => {
+ if (!key.startsWith(WAREHOUSE_AREAS_ITEM_DYNAMIC_FIELD_PREFIX)) {
+ return
+ }
+ const normalizedValue = normalizeText(value)
+ if (normalizedValue) {
+ result[key.slice(WAREHOUSE_AREAS_ITEM_DYNAMIC_FIELD_PREFIX.length)] = normalizedValue
+ }
+ })
+
+ return result
+}
+
+export function buildWarehouseAreasItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWarehouseAreasItemSearchParams(params)
+ }
+}
+
+export function buildWarehouseAreasItemIsptQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...(params.id !== undefined ? { id: params.id } : {})
+ }
+}
+
+export function attachWarehouseAreasItemDynamicFields(record = {}, enabledFields = []) {
+ const extendFields = record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
+ const dynamicValues = {}
+
+ enabledFields.forEach((field) => {
+ dynamicValues[getWarehouseAreasItemDynamicFieldKey(field.fields)] = extendFields[field.fields] || ''
+ })
+
+ return {
+ ...record,
+ ...dynamicValues
+ }
+}
+
+export function normalizeWarehouseAreasItemRow(record = {}, enabledFields = []) {
+ return attachWarehouseAreasItemDynamicFields(
+ {
+ ...record,
+ areaName: record.areaName || '-',
+ asnCode: record.asnCode || '-',
+ platWorkCode: record.platWorkCode || '-',
+ platOrderCode: record.platOrderCode || '-',
+ projectCode: record.projectCode || '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || record.matnrName || '-',
+ splrBatch: record.splrBatch || record.splrBtch || '-',
+ batch: record.batch || '-',
+ barcode: record.trackCode || record.barcode || '-',
+ unit: record.unit || '-',
+ anfme: normalizeNumber(record.anfme),
+ workQty: normalizeNumber(record.workQty),
+ qty: normalizeNumber(record.qty),
+ isptResultText: record['isptResult$'] || '-',
+ statusText:
+ record.statusBool === true || Number(record.status) === 1
+ ? '鍚敤'
+ : record.statusBool === false || Number(record.status) === 0
+ ? '鍋滅敤'
+ : '-',
+ updateTimeText: record['updateTime$'] || record.updateTime || '-',
+ createTimeText: record['createTime$'] || record.createTime || '-'
+ },
+ enabledFields
+ )
+}
+
+export function normalizeWarehouseAreasItemIsptRow(record = {}) {
+ return {
+ ...record,
+ id: record.id,
+ ispectId: record.ispectId ?? '-',
+ matnrCode: record.matnrCode || '-',
+ maktx: record.maktx || '-',
+ label: record.label || '-',
+ splrBatch: record.splrBatch || '-',
+ dlyQty: normalizeNumber(record.dlyQty),
+ anfme: normalizeNumber(record.anfme),
+ splrName: record.splrName || '-',
+ isptResultText: record['isptResult$'] || record.isptResult$ || '-'
+ }
+}
+
+export function buildWarehouseAreasItemPrintRows(records = [], enabledFields = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((record) => normalizeWarehouseAreasItemRow(record, enabledFields))
+}
+
+export function buildWarehouseAreasItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ orientation = WAREHOUSE_AREAS_ITEM_REPORT_STYLE.orientation
+} = {}) {
+ return {
+ reportTitle: WAREHOUSE_AREAS_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ ...WAREHOUSE_AREAS_ITEM_REPORT_STYLE,
+ orientation
+ }
+ }
+}
diff --git a/rsf-design/src/views/stock/warehouse-areas-item/warehouseAreasItemTable.columns.js b/rsf-design/src/views/stock/warehouse-areas-item/warehouseAreasItemTable.columns.js
new file mode 100644
index 0000000..c605947
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-areas-item/warehouseAreasItemTable.columns.js
@@ -0,0 +1,113 @@
+import { h } from 'vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createWarehouseAreasItemTableColumns({
+ enabledFields = [],
+ handleViewIspt
+}) {
+ const dynamicColumns = enabledFields.map((field) => ({
+ prop: `extendField__${field.fields}`,
+ label: field.fieldsAlise,
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row[`extendField__${field.fields}`] || '-'
+ }))
+
+ return [
+ {
+ type: 'selection',
+ width: 48,
+ align: 'center'
+ },
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'areaName',
+ label: '搴撳尯鍚嶇О',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'asnCode',
+ label: '璁″垝璺熻釜鍙�',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'platWorkCode',
+ label: '琛屽彿',
+ width: 90,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '鎵规',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '搴旀敹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '瀹炴敹鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120,
+ align: 'right'
+ },
+ ...dynamicColumns,
+ {
+ prop: 'isptResultText',
+ label: '璐ㄦ缁撴灉',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ type: 'view',
+ text: '璐ㄦ缁撴灉',
+ onClick: () => handleViewIspt(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/stock/warehouse-stock/index.vue b/rsf-design/src/views/stock/warehouse-stock/index.vue
new file mode 100644
index 0000000..e6ab489
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-stock/index.vue
@@ -0,0 +1,503 @@
+<template>
+ <div class="warehouse-stock-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <WarehouseStockDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :summary="activeStockSummary"
+ :data="detailTableData"
+ :columns="detailColumns"
+ :pagination="detailPagination"
+ @refresh="refreshDetailData"
+ @size-change="handleDetailSizeChange"
+ @current-change="handleDetailCurrentChange"
+ />
+
+ <WarehouseStockHistoriesDrawer
+ v-model:visible="historiesDrawerVisible"
+ :loading="historiesLoading"
+ :summary="activeStockSummary"
+ :data="historiesTableData"
+ :columns="historiesColumns"
+ :pagination="historiesPagination"
+ @refresh="refreshHistoriesData"
+ @size-change="handleHistoriesSizeChange"
+ @current-change="handleHistoriesCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from 'vue'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchEnabledFields,
+ fetchWarehouseStockHistoriesPage,
+ fetchWarehouseStockInfoPage,
+ fetchWarehouseStockPage
+ } from '@/api/warehouse-stock'
+ import WarehouseStockDetailDrawer from './modules/warehouse-stock-detail-drawer.vue'
+ import WarehouseStockHistoriesDrawer from './modules/warehouse-stock-histories-drawer.vue'
+ import { createWarehouseStockTableColumns } from './warehouseStockTable.columns'
+ import {
+ buildWarehouseStockDetailQueryParams,
+ buildWarehouseStockHistoriesQueryParams,
+ buildWarehouseStockPageQueryParams,
+ createWarehouseStockSearchState,
+ getWarehouseStockAggTypeOptions,
+ getWarehouseStockDynamicFieldKey,
+ getWarehouseStockPaginationKey,
+ normalizeWarehouseEnabledFields,
+ normalizeWarehouseStockDetailRow,
+ normalizeWarehouseStockHistoryRow,
+ normalizeWarehouseStockRow
+ } from './warehouseStockPage.helpers'
+
+ defineOptions({ name: 'WarehouseStock' })
+
+ const searchForm = ref(createWarehouseStockSearchState())
+ const loading = ref(false)
+ const tableData = ref([])
+ const enabledFields = ref([])
+ const activeStockSummary = ref({})
+
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailTableData = ref([])
+
+ const historiesDrawerVisible = ref(false)
+ const historiesLoading = ref(false)
+ const historiesTableData = ref([])
+
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const detailPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const historiesPagination = reactive({
+ current: 1,
+ size: 20,
+ total: 0
+ })
+
+ const paginationKey = getWarehouseStockPaginationKey()
+
+ const searchItems = computed(() => [
+ {
+ label: '姹囨�荤被鍨�',
+ key: 'aggType',
+ type: 'select',
+ props: {
+ clearable: false,
+ options: getWarehouseStockAggTypeOptions()
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ },
+ ...enabledFields.value.map((field) => ({
+ label: field.fieldsAlise,
+ key: getWarehouseStockDynamicFieldKey(field.fields),
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: `璇疯緭鍏�${field.fieldsAlise}`
+ }
+ }))
+ ])
+
+ function createDynamicFieldColumns() {
+ return enabledFields.value.map((field) => ({
+ prop: getWarehouseStockDynamicFieldKey(field.fields),
+ label: field.fieldsAlise,
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row[getWarehouseStockDynamicFieldKey(field.fields)] || '-'
+ }))
+ }
+
+ function createDetailColumns() {
+ return [
+ {
+ prop: 'warehouseLabel',
+ label: '浠撳簱',
+ minWidth: 120,
+ formatter: (row) => row.warehouseLabel || '-'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ formatter: (row) => row.batch || '-'
+ },
+ {
+ prop: 'anfme',
+ label: '鍙敤搴撳瓨',
+ width: 120,
+ formatter: (row) => row.anfme ?? 0
+ },
+ {
+ prop: 'qty',
+ label: '搴撳瓨鏁伴噺',
+ width: 120,
+ formatter: (row) => row.qty ?? 0
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100,
+ formatter: (row) => row.unit || '-'
+ },
+ ...createDynamicFieldColumns()
+ ]
+ }
+
+ function createHistoriesColumns() {
+ return [
+ {
+ prop: 'stockCode',
+ label: '鍗曟嵁缂栧彿',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.stockCode || '-'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ formatter: (row) => row.batch || '-'
+ },
+ {
+ prop: 'anfme',
+ label: '搴撳瓨鏁伴噺',
+ width: 120,
+ formatter: (row) => row.anfme ?? 0
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓暟閲�',
+ width: 120,
+ formatter: (row) => row.workQty ?? 0
+ },
+ {
+ prop: 'qty',
+ label: '宸叉敹鏁伴噺',
+ width: 120,
+ formatter: (row) => row.qty ?? 0
+ },
+ {
+ prop: 'stockUnit',
+ label: '鍗曚綅',
+ width: 100,
+ formatter: (row) => row.stockUnit || '-'
+ },
+ ...createDynamicFieldColumns(),
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.createTimeText || '-'
+ }
+ ]
+ }
+
+ const detailColumns = computed(() => createDetailColumns())
+ const historiesColumns = computed(() => createHistoriesColumns())
+
+ function openDetailDrawer(row) {
+ activeStockSummary.value = row
+ detailDrawerVisible.value = true
+ detailPagination.current = 1
+ loadDetailData()
+ }
+
+ function openHistoriesDrawer(row) {
+ activeStockSummary.value = row
+ historiesDrawerVisible.value = true
+ historiesPagination.current = 1
+ loadHistoriesData()
+ }
+
+ const { columns, columnChecks, resetColumns } = useTableColumns(() =>
+ createWarehouseStockTableColumns({
+ enabledFields: enabledFields.value,
+ handleViewDetail: openDetailDrawer,
+ handleViewHistories: openHistoriesDrawer
+ })
+ )
+
+ async function loadEnabledFieldDefinitions() {
+ const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
+ timeoutMessage: '鎵╁睍瀛楁鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ enabledFields.value = normalizeWarehouseEnabledFields(fields)
+ enabledFields.value.forEach((field) => {
+ const dynamicKey = getWarehouseStockDynamicFieldKey(field.fields)
+ if (searchForm.value[dynamicKey] === undefined) {
+ searchForm.value[dynamicKey] = ''
+ }
+ })
+ resetColumns()
+ }
+
+ function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
+ target.total = Number(response?.total || 0)
+ target.current = Number(response?.current || fallbackCurrent || 1)
+ target.size = Number(response?.size || fallbackSize || target.size || 20)
+ }
+
+ async function loadPageData() {
+ loading.value = true
+ const query = buildWarehouseStockPageQueryParams({
+ ...searchForm.value,
+ [paginationKey.current]: pagination.current,
+ [paginationKey.size]: pagination.size
+ })
+
+ try {
+ const response = await guardRequestWithMessage(
+ fetchWarehouseStockPage(query),
+ {
+ records: [],
+ total: 0,
+ current: pagination.current,
+ size: pagination.size
+ },
+ { timeoutMessage: '鍗虫椂搴撳瓨鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ tableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeWarehouseStockRow(record, enabledFields.value))
+ : []
+ updatePaginationState(pagination, response, pagination.current, pagination.size)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function loadDetailData() {
+ if (!detailDrawerVisible.value || !activeStockSummary.value) {
+ return
+ }
+
+ detailLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchWarehouseStockInfoPage(
+ buildWarehouseStockDetailQueryParams({
+ current: detailPagination.current,
+ pageSize: detailPagination.size,
+ aggType: searchForm.value.aggType,
+ stock: activeStockSummary.value
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: detailPagination.current,
+ size: detailPagination.size
+ },
+ { timeoutMessage: '搴撳瓨璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+
+ detailTableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeWarehouseStockDetailRow(record, enabledFields.value))
+ : []
+ updatePaginationState(detailPagination, response, detailPagination.current, detailPagination.size)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function loadHistoriesData() {
+ if (!historiesDrawerVisible.value || !activeStockSummary.value) {
+ return
+ }
+
+ historiesLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchWarehouseStockHistoriesPage(
+ buildWarehouseStockHistoriesQueryParams({
+ current: historiesPagination.current,
+ pageSize: historiesPagination.size,
+ aggType: searchForm.value.aggType,
+ stock: activeStockSummary.value
+ })
+ ),
+ {
+ records: [],
+ total: 0,
+ current: historiesPagination.current,
+ size: historiesPagination.size
+ },
+ { timeoutMessage: '搴撳瓨鍘嗗彶鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+
+ historiesTableData.value = Array.isArray(response?.records)
+ ? response.records.map((record) => normalizeWarehouseStockHistoryRow(record, enabledFields.value))
+ : []
+ updatePaginationState(
+ historiesPagination,
+ response,
+ historiesPagination.current,
+ historiesPagination.size
+ )
+ } finally {
+ historiesLoading.value = false
+ }
+ }
+
+ async function refreshData() {
+ await loadPageData()
+ }
+
+ async function refreshDetailData() {
+ await loadDetailData()
+ }
+
+ async function refreshHistoriesData() {
+ await loadHistoriesData()
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleReset() {
+ const resetState = createWarehouseStockSearchState()
+ Object.keys(searchForm.value).forEach((key) => {
+ if (key.startsWith('extendField__')) {
+ searchForm.value[key] = ''
+ }
+ })
+ Object.assign(searchForm.value, resetState)
+ pagination.current = 1
+ pagination.size = 20
+ loadPageData()
+ }
+
+ function handleSizeChange(size) {
+ pagination.size = size
+ pagination.current = 1
+ loadPageData()
+ }
+
+ function handleCurrentChange(current) {
+ pagination.current = current
+ loadPageData()
+ }
+
+ function handleDetailSizeChange(size) {
+ detailPagination.size = size
+ detailPagination.current = 1
+ loadDetailData()
+ }
+
+ function handleDetailCurrentChange(current) {
+ detailPagination.current = current
+ loadDetailData()
+ }
+
+ function handleHistoriesSizeChange(size) {
+ historiesPagination.size = size
+ historiesPagination.current = 1
+ loadHistoriesData()
+ }
+
+ function handleHistoriesCurrentChange(current) {
+ historiesPagination.current = current
+ loadHistoriesData()
+ }
+
+ onMounted(async () => {
+ await loadEnabledFieldDefinitions()
+ await loadPageData()
+ })
+</script>
diff --git a/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-detail-drawer.vue b/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-detail-drawer.vue
new file mode 100644
index 0000000..9d44483
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-detail-drawer.vue
@@ -0,0 +1,47 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳瓨璇︽儏"
+ size="80%"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ summary.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ summary.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱">{{ summary.warehouseLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ summary.batch || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="flex justify-end">
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ summary: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue b/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue
new file mode 100644
index 0000000..0c12fe9
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue
@@ -0,0 +1,48 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="搴撳瓨鍘嗗彶"
+ size="80%"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="鍗曟嵁缂栧彿">{{ data[0]?.stockCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ summary.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ summary.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠撳簱">{{ summary.warehouseLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ summary.batch || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <div class="flex justify-end">
+ <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
+ </div>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="$emit('size-change', $event)"
+ @pagination:current-change="$emit('current-change', $event)"
+ />
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ summary: { type: Object, default: () => ({}) },
+ data: { type: Array, default: () => [] },
+ columns: { type: Array, default: () => [] },
+ pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
+ })
+
+ const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js b/rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js
new file mode 100644
index 0000000..09e27a6
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js
@@ -0,0 +1,186 @@
+export const WAREHOUSE_STOCK_REPORT_TITLE = '鍗虫椂搴撳瓨鎶ヨ〃'
+export const WAREHOUSE_STOCK_DYNAMIC_FIELD_PREFIX = 'extendField__'
+
+const AGG_TYPE_OPTIONS = [
+ { label: '鎸夌墿鏂欐眹鎬�', value: 'matnr' },
+ { label: '鎸変緵搴斿晢姹囨��', value: 'supplier' },
+ { label: '鎸変粨搴撴眹鎬�', value: 'warehouse' },
+ { label: '鎸夋壒娆℃眹鎬�', value: 'batch' },
+ { label: '鎸夊姩鎬佹墿灞曞瓧娈垫眹鎬�', value: 'fieldsIndex' }
+]
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+export function createWarehouseStockSearchState() {
+ return {
+ aggType: 'matnr',
+ matnrCode: '',
+ maktx: '',
+ batch: ''
+ }
+}
+
+export function getWarehouseStockPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getWarehouseStockAggTypeOptions() {
+ return AGG_TYPE_OPTIONS
+}
+
+export function getWarehouseStockDynamicFieldKey(fieldName) {
+ return `${WAREHOUSE_STOCK_DYNAMIC_FIELD_PREFIX}${fieldName}`
+}
+
+export function normalizeWarehouseEnabledFields(fields = []) {
+ if (!Array.isArray(fields)) {
+ return []
+ }
+ return fields
+ .map((item) => ({
+ fields: normalizeText(item.fields),
+ fieldsAlise: normalizeText(item.fieldsAlise || item.fieldsAlias || item.fields)
+ }))
+ .filter((item) => item.fields)
+}
+
+export function buildWarehouseStockSearchParams(params = {}) {
+ const result = {
+ aggType: normalizeText(params.aggType) || 'matnr'
+ }
+
+ const matnrCode = normalizeText(params.matnrCode)
+ const maktx = normalizeText(params.maktx)
+ const batch = normalizeText(params.batch)
+
+ if (matnrCode) {
+ result.matnrCode = matnrCode
+ }
+ if (maktx) {
+ result.maktx = maktx
+ }
+ if (batch) {
+ result.batch = batch
+ }
+
+ Object.entries(params).forEach(([key, value]) => {
+ if (!key.startsWith(WAREHOUSE_STOCK_DYNAMIC_FIELD_PREFIX)) {
+ return
+ }
+ const normalizedValue = normalizeText(value)
+ if (normalizedValue) {
+ result[key.slice(WAREHOUSE_STOCK_DYNAMIC_FIELD_PREFIX.length)] = normalizedValue
+ }
+ })
+
+ return result
+}
+
+export function buildWarehouseStockPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildWarehouseStockSearchParams(params)
+ }
+}
+
+export function buildWarehouseStockDetailQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ aggType: normalizeText(params.aggType) || 'matnr',
+ stock: params.stock || {}
+ }
+}
+
+export function buildWarehouseStockHistoriesQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ aggType: normalizeText(params.aggType) || 'matnr',
+ stock: params.stock || {}
+ }
+}
+
+export function attachWarehouseStockDynamicFields(record = {}, enabledFields = []) {
+ const extendFields = record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
+ const dynamicValues = {}
+ enabledFields.forEach((field) => {
+ dynamicValues[getWarehouseStockDynamicFieldKey(field.fields)] = extendFields[field.fields] || ''
+ })
+ return {
+ ...record,
+ ...dynamicValues
+ }
+}
+
+export function normalizeWarehouseStockRow(record = {}, enabledFields = []) {
+ return attachWarehouseStockDynamicFields(
+ {
+ ...record,
+ warehouseLabel: record['warehouse$'] || record.warehouse || '',
+ matnrCode: record.matnrCode || '',
+ maktx: record.maktx || '',
+ batch: record.batch || '',
+ unit: record.unit || '',
+ spec: record.spec || '',
+ model: record.model || '',
+ fieldsIndex: record.fieldsIndex || '',
+ anfme: normalizeNumber(record.anfme),
+ qty: normalizeNumber(record.qty),
+ workQty: normalizeNumber(record.workQty),
+ updateTimeText: record['updateTime$'] || record.updateTime || ''
+ },
+ enabledFields
+ )
+}
+
+export function normalizeWarehouseStockDetailRow(record = {}, enabledFields = []) {
+ return attachWarehouseStockDynamicFields(
+ {
+ ...record,
+ warehouseLabel: record['warehouse$'] || record.warehouse || '',
+ locCode: record.locCode || '',
+ matnrCode: record.matnrCode || '',
+ maktx: record.maktx || '',
+ batch: record.batch || '',
+ unit: record.unit || '',
+ qty: normalizeNumber(record.qty),
+ anfme: normalizeNumber(record.anfme),
+ updateTimeText: record['updateTime$'] || record.updateTime || ''
+ },
+ enabledFields
+ )
+}
+
+export function normalizeWarehouseStockHistoryRow(record = {}, enabledFields = []) {
+ return attachWarehouseStockDynamicFields(
+ {
+ ...record,
+ stockCode: record.stockCode || record.orderCode || '',
+ orderCode: record.orderCode || '',
+ matnrCode: record.matnrCode || '',
+ maktx: record.maktx || '',
+ batch: record.batch || '',
+ stockUnit: record.stockUnit || record.unit || '',
+ qty: normalizeNumber(record.qty),
+ workQty: normalizeNumber(record.workQty),
+ createTimeText: record['createTime$'] || record.createTime || '',
+ updateTimeText: record['updateTime$'] || record.updateTime || ''
+ },
+ enabledFields
+ )
+}
diff --git a/rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js b/rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js
new file mode 100644
index 0000000..c69ed19
--- /dev/null
+++ b/rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js
@@ -0,0 +1,93 @@
+import { h } from 'vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createWarehouseStockTableColumns({
+ enabledFields = [],
+ handleViewDetail,
+ handleViewHistories
+}) {
+ const dynamicColumns = enabledFields.map((field) => ({
+ prop: `extendField__${field.fields}`,
+ label: field.fieldsAlise,
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row[`extendField__${field.fields}`] || '-'
+ }))
+
+ return [
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'warehouseLabel',
+ label: '浠撳簱',
+ minWidth: 150,
+ formatter: (row) => row.warehouseLabel || '-'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 140,
+ formatter: (row) => row.batch || '-'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 100,
+ formatter: (row) => row.unit || '-'
+ },
+ {
+ prop: 'anfme',
+ label: '鍙敤搴撳瓨',
+ width: 120,
+ formatter: (row) => row.anfme ?? 0
+ },
+ {
+ prop: 'qty',
+ label: '搴撳瓨鏁伴噺',
+ width: 120,
+ formatter: (row) => row.qty ?? 0
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц涓簱瀛�',
+ width: 120,
+ formatter: (row) => row.workQty ?? 0
+ },
+ ...dynamicColumns,
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鍘嗗彶璁板綍',
+ width: 130,
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', { class: 'flex justify-end gap-2' }, [
+ h(ArtButtonTable, {
+ type: 'view',
+ text: '搴撳瓨璇︽儏',
+ onClick: () => handleViewDetail(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'view',
+ text: '鍘嗗彶璁板綍',
+ onClick: () => handleViewHistories(row)
+ })
+ ])
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/ai-mcp-mount/aiMcpMountPage.helpers.js b/rsf-design/src/views/system/ai-mcp-mount/aiMcpMountPage.helpers.js
new file mode 100644
index 0000000..182c51c
--- /dev/null
+++ b/rsf-design/src/views/system/ai-mcp-mount/aiMcpMountPage.helpers.js
@@ -0,0 +1,122 @@
+export function createAiMcpMountSearchState() {
+ return {
+ condition: '',
+ transportType: '',
+ status: ''
+ }
+}
+
+export function getAiMcpMountPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getAiMcpMountTransportOptions() {
+ return [
+ { label: 'SSE_HTTP', value: 'SSE_HTTP' },
+ { label: 'STDIO', value: 'STDIO' },
+ { label: 'BUILTIN', value: 'BUILTIN' }
+ ]
+}
+
+export function getAiMcpMountHealthMeta(healthStatus) {
+ if (healthStatus === 'HEALTHY') {
+ return { text: '鍋ュ悍', type: 'success' }
+ }
+ if (healthStatus === 'UNHEALTHY') {
+ return { text: '寮傚父', type: 'danger' }
+ }
+ return { text: '鏈祴璇�', type: 'info' }
+}
+
+export function buildAiMcpMountSearchParams(params = {}) {
+ return {
+ condition: params.condition?.trim?.() || '',
+ transportType: params.transportType || '',
+ status: params.status === '' || params.status === undefined || params.status === null ? '' : Number(params.status)
+ }
+}
+
+export function buildAiMcpMountPageQueryParams(params = {}) {
+ const normalized = buildAiMcpMountSearchParams(params)
+ return {
+ current: Number(params.current || 1),
+ pageSize: Number(params.pageSize || params.size || 20),
+ condition: normalized.condition,
+ transportType: normalized.transportType,
+ status: normalized.status === '' ? null : normalized.status
+ }
+}
+
+export function buildAiMcpMountDialogModel(record = {}) {
+ return {
+ id: record.id ?? null,
+ name: record.name || '',
+ transportType: record.transportType || 'SSE_HTTP',
+ builtinCode: record.builtinCode || '',
+ serverUrl: record.serverUrl || '',
+ endpoint: record.endpoint || '/sse',
+ command: record.command || '',
+ argsJson: record.argsJson || '',
+ envJson: record.envJson || '',
+ headersJson: record.headersJson || '',
+ requestTimeoutMs: record.requestTimeoutMs ?? 60000,
+ healthStatus: record.healthStatus || 'NOT_TESTED',
+ 'lastTestTime$': record['lastTestTime$'] || '',
+ lastTestMessage: record.lastTestMessage || '',
+ lastInitElapsedMs: record.lastInitElapsedMs ?? null,
+ sort: record.sort ?? 0,
+ status: record.status ?? 1,
+ memo: record.memo || '',
+ updateBy: record.updateBy || '',
+ 'updateTime$': record['updateTime$'] || ''
+ }
+}
+
+export function buildAiMcpMountSavePayload(form = {}) {
+ return {
+ id: form.id ?? null,
+ name: form.name?.trim?.() || '',
+ transportType: form.transportType || 'SSE_HTTP',
+ builtinCode: form.builtinCode?.trim?.() || '',
+ serverUrl: form.serverUrl?.trim?.() || '',
+ endpoint: form.endpoint?.trim?.() || '/sse',
+ command: form.command?.trim?.() || '',
+ argsJson: form.argsJson?.trim?.() || '',
+ envJson: form.envJson?.trim?.() || '',
+ headersJson: form.headersJson?.trim?.() || '',
+ requestTimeoutMs: Number(form.requestTimeoutMs || 60000),
+ sort: Number(form.sort || 0),
+ status: Number(form.status ?? 1),
+ memo: form.memo?.trim?.() || ''
+ }
+}
+
+export function normalizeAiMcpMountRow(row = {}) {
+ const healthMeta = getAiMcpMountHealthMeta(row.healthStatus)
+ const statusBool = row.statusBool ?? row.status === 1
+ const targetLabel = row.transportType === 'BUILTIN'
+ ? row.builtinCode || '--'
+ : row.transportType === 'STDIO'
+ ? row.command || '--'
+ : row.serverUrl || '--'
+
+ return {
+ ...row,
+ endpoint: row.endpoint || '/sse',
+ command: row.command || '',
+ serverUrl: row.serverUrl || '',
+ memo: row.memo || '',
+ statusBool,
+ statusText: statusBool ? '鍚敤' : '鍋滅敤',
+ statusType: statusBool ? 'success' : 'info',
+ healthText: healthMeta.text,
+ healthType: healthMeta.type,
+ transportText: row['transportType$'] || row.transportType || '--',
+ targetLabel,
+ 'lastTestTime$': row['lastTestTime$'] || '',
+ 'updateTime$': row['updateTime$'] || ''
+ }
+}
diff --git a/rsf-design/src/views/system/ai-mcp-mount/index.vue b/rsf-design/src/views/system/ai-mcp-mount/index.vue
new file mode 100644
index 0000000..9170481
--- /dev/null
+++ b/rsf-design/src/views/system/ai-mcp-mount/index.vue
@@ -0,0 +1,347 @@
+<template>
+ <div class="ai-mcp-mount-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <div class="mb-5 flex flex-wrap items-center justify-between gap-4">
+ <div>
+ <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">MCP 鎸傝浇</h3>
+ <p class="mt-1 text-sm text-[var(--art-gray-500)]">鎸変紶杈撶被鍨嬬鐞� MCP 鎸傝浇銆佽繛閫氭�у拰宸ュ叿棰勮銆�</p>
+ </div>
+
+ <ElSpace wrap>
+ <ElButton v-auth="'save'" @click="openCreateDialog" v-ripple>鏂板缓鎸傝浇</ElButton>
+ <ElButton :loading="loading" @click="refreshData" v-ripple>鍒锋柊</ElButton>
+ </ElSpace>
+ </div>
+
+ <div v-loading="loading" class="space-y-6">
+ <ElEmpty v-if="!groupedRecords.length" description="鏆傛棤 MCP 鎸傝浇鏁版嵁" :image-size="110" />
+
+ <section v-for="group in groupedRecords" :key="group.key" class="space-y-4">
+ <div>
+ <h4 class="text-base font-semibold text-[var(--art-gray-900)]">{{ group.title }}</h4>
+ <p class="mt-1 text-sm text-[var(--art-gray-500)]">{{ group.description }}</p>
+ </div>
+
+ <div class="grid gap-5 md:grid-cols-2 2xl:grid-cols-3">
+ <article
+ v-for="item in group.records"
+ :key="item.id"
+ class="rounded-3xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-5 shadow-[0_12px_40px_rgba(15,23,42,0.04)]"
+ >
+ <div class="flex items-start justify-between gap-4">
+ <div class="min-w-0">
+ <div class="flex items-center gap-3">
+ <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-emerald-50 text-emerald-600">
+ <ArtSvgIcon icon="ri:plug-2-line" class="text-xl" />
+ </div>
+ <div class="min-w-0">
+ <h4 class="truncate text-base font-semibold text-[var(--art-gray-900)]">{{ item.name || '--' }}</h4>
+ <p class="mt-1 truncate text-sm text-[var(--art-gray-500)]">{{ item.transportText }}</p>
+ </div>
+ </div>
+ </div>
+
+ <div class="flex flex-wrap justify-end gap-2">
+ <ElTag :type="item.statusType" effect="light">{{ item.statusText }}</ElTag>
+ <ElTag :type="item.healthType" effect="light">{{ item.healthText }}</ElTag>
+ </div>
+ </div>
+
+ <div class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
+ <div class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]">
+ <p class="text-xs text-[var(--art-gray-500)]">鐩爣鍦板潃</p>
+ <p class="mt-2 break-all text-[var(--art-gray-900)]">{{ item.targetLabel || '--' }}</p>
+ </div>
+ <div class="rounded-2xl bg-[var(--art-main-bg-color)]/70 p-3 ring-1 ring-inset ring-[var(--art-border-color)]">
+ <p class="text-xs text-[var(--art-gray-500)]">鏈�杩戞祴璇�</p>
+ <p class="mt-2 text-[var(--art-gray-900)]">{{ item['lastTestTime$'] || '鏈祴璇�' }}</p>
+ </div>
+ </div>
+
+ <div class="mt-4 grid gap-3 text-sm sm:grid-cols-3">
+ <div class="rounded-2xl bg-slate-50 px-3 py-2">
+ <p class="text-xs text-[var(--art-gray-500)]">瓒呮椂</p>
+ <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.requestTimeoutMs ?? '--' }} ms</p>
+ </div>
+ <div class="rounded-2xl bg-slate-50 px-3 py-2">
+ <p class="text-xs text-[var(--art-gray-500)]">鎺掑簭</p>
+ <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.sort ?? '--' }}</p>
+ </div>
+ <div class="rounded-2xl bg-slate-50 px-3 py-2">
+ <p class="text-xs text-[var(--art-gray-500)]">鍒濆鍖栬�楁椂</p>
+ <p class="mt-1 font-medium text-[var(--art-gray-900)]">{{ item.lastInitElapsedMs ?? '--' }}</p>
+ </div>
+ </div>
+
+ <div class="mt-4 rounded-2xl bg-amber-50/80 px-4 py-3">
+ <p class="text-xs text-[var(--art-gray-500)]">澶囨敞</p>
+ <p class="mt-2 line-clamp-3 text-sm leading-6 text-[var(--art-gray-900)]">{{ item.memo || '--' }}</p>
+ </div>
+
+ <div class="mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-[var(--art-border-color)] pt-4">
+ <div class="text-xs text-[var(--art-gray-500)]">{{ item['updateTime$'] || '--' }}</div>
+
+ <ElSpace wrap>
+ <ElButton text @click="openDetailDialog(item)">璇︽儏</ElButton>
+ <ElButton v-auth="'update'" text @click="openEditDialog(item)">缂栬緫</ElButton>
+ <ElButton
+ v-auth="'update'"
+ text
+ :loading="connectivityTestingId === item.id"
+ @click="handleConnectivityTest(item)"
+ >
+ 杩為�氭�ф祴璇�
+ </ElButton>
+ <ElButton v-auth="'list'" text @click="openToolsDrawer(item)">宸ュ叿棰勮</ElButton>
+ <ElButton v-auth="'remove'" text type="danger" @click="handleDelete(item)">鍒犻櫎</ElButton>
+ </ElSpace>
+ </div>
+ </article>
+ </div>
+ </section>
+ </div>
+
+ <div class="mt-6 flex justify-end">
+ <ElPagination
+ background
+ layout="total, sizes, prev, pager, next, jumper"
+ :current-page="pagination.current"
+ :page-size="pagination.size"
+ :total="pagination.total"
+ :page-sizes="[20, 50, 100]"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+
+ <AiMcpMountDialog
+ v-model:visible="dialogVisible"
+ :mode="dialogMode"
+ :mcp-mount-data="currentMcpMountData"
+ @submit="handleDialogSubmit"
+ />
+
+ <AiMcpToolsDrawer
+ v-model:visible="toolsDrawerVisible"
+ :mount-id="currentToolsMount.id"
+ :mount-name="currentToolsMount.name"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchAiMcpMountPage,
+ fetchDeleteAiMcpMount,
+ fetchGetAiMcpMountDetail,
+ fetchSaveAiMcpMount,
+ fetchTestAiMcpConnectivity,
+ fetchUpdateAiMcpMount
+ } from '@/api/ai-config'
+ import AiMcpMountDialog from './modules/ai-mcp-mount-dialog.vue'
+ import AiMcpToolsDrawer from './modules/ai-mcp-tools-drawer.vue'
+ import {
+ buildAiMcpMountDialogModel,
+ buildAiMcpMountPageQueryParams,
+ buildAiMcpMountSearchParams,
+ createAiMcpMountSearchState,
+ getAiMcpMountPaginationKey,
+ normalizeAiMcpMountRow
+ } from './aiMcpMountPage.helpers'
+
+ defineOptions({ name: 'AiMcpMount' })
+
+ const searchForm = ref(createAiMcpMountSearchState())
+ const dialogVisible = ref(false)
+ const dialogMode = ref('create')
+ const currentMcpMountData = ref(buildAiMcpMountDialogModel())
+ const toolsDrawerVisible = ref(false)
+ const currentToolsMount = ref({ id: null, name: '' })
+ const connectivityTestingId = ref(null)
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悕绉�'
+ }
+ },
+ {
+ label: '浼犺緭绫诲瀷',
+ key: 'transportType',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: 'SSE_HTTP', value: 'SSE_HTTP' },
+ { label: 'STDIO', value: 'STDIO' },
+ { label: 'BUILTIN', value: 'BUILTIN' }
+ ]
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鍚敤', value: 1 },
+ { label: '鍋滅敤', value: 0 }
+ ]
+ }
+ }
+ ])
+
+ const {
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchAiMcpMountPage,
+ apiParams: buildAiMcpMountPageQueryParams(searchForm.value),
+ paginationKey: getAiMcpMountPaginationKey()
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeAiMcpMountRow(item))
+ }
+ }
+ })
+
+ const groupedRecords = computed(() => {
+ const groups = [
+ { key: 'BUILTIN', title: '鍐呯疆 MCP', description: '绯荤粺鍐呯疆鐨勬爣鍑� MCP 鎸傝浇銆�' },
+ { key: 'SSE_HTTP', title: 'SSE / HTTP', description: '閫氳繃鏈嶅姟鍦板潃鍜� SSE 绔偣鎺ュ叆鐨� MCP銆�' },
+ { key: 'STDIO', title: 'STDIO', description: '閫氳繃鏈湴鍛戒护鍚姩鐨� MCP銆�' }
+ ]
+ return groups
+ .map((group) => ({
+ ...group,
+ records: data.value.filter((item) => item.transportType === group.key)
+ }))
+ .filter((group) => group.records.length > 0)
+ })
+
+ async function openEditDialog(record) {
+ try {
+ currentMcpMountData.value = buildAiMcpMountDialogModel(await fetchGetAiMcpMountDetail(record.id))
+ dialogMode.value = 'edit'
+ dialogVisible.value = true
+ } catch {
+ return
+ }
+ }
+
+ async function openDetailDialog(record) {
+ try {
+ currentMcpMountData.value = buildAiMcpMountDialogModel(await fetchGetAiMcpMountDetail(record.id))
+ dialogMode.value = 'show'
+ dialogVisible.value = true
+ } catch {
+ return
+ }
+ }
+
+ function openCreateDialog() {
+ currentMcpMountData.value = buildAiMcpMountDialogModel()
+ dialogMode.value = 'create'
+ dialogVisible.value = true
+ }
+
+ function openToolsDrawer(record) {
+ currentToolsMount.value = {
+ id: record.id,
+ name: record.name || ''
+ }
+ toolsDrawerVisible.value = true
+ }
+
+ async function handleDialogSubmit(payload) {
+ try {
+ if (dialogMode.value === 'edit') {
+ await fetchUpdateAiMcpMount(payload)
+ ElMessage.success('淇敼鎴愬姛')
+ dialogVisible.value = false
+ await refreshUpdate()
+ return
+ }
+ await fetchSaveAiMcpMount(payload)
+ ElMessage.success('鏂板鎴愬姛')
+ dialogVisible.value = false
+ await refreshCreate()
+ } catch {
+ return
+ }
+ }
+
+ async function handleDelete(record) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄ゆ寕杞姐��${record.name || record.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteAiMcpMount(record.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshRemove()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ async function handleConnectivityTest(record) {
+ connectivityTestingId.value = record.id
+ try {
+ const result = await guardRequestWithMessage(fetchTestAiMcpConnectivity(record.id), null, {
+ timeoutMessage: '杩為�氭�ф祴璇曡秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ ElMessage.success(result?.message || '杩為�氭�ф祴璇曟垚鍔�')
+ await refreshUpdate()
+ } catch (error) {
+ ElMessage.error(error?.message || '杩為�氭�ф祴璇曞け璐�')
+ } finally {
+ connectivityTestingId.value = null
+ }
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildAiMcpMountSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createAiMcpMountSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-mount-dialog.vue b/rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-mount-dialog.vue
new file mode 100644
index 0000000..eba8b9a
--- /dev/null
+++ b/rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-mount-dialog.vue
@@ -0,0 +1,292 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="900px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ :disabled="isReadonly"
+ />
+
+ <div v-if="!isReadonly" class="mt-4 rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4">
+ <div class="flex flex-wrap items-center justify-between gap-3">
+ <div>
+ <div class="text-sm font-semibold text-[var(--art-gray-900)]">鑽夌杩為�氭�ф祴璇�</div>
+ <div class="mt-1 text-xs text-[var(--art-gray-500)]">淇濆瓨鍓嶅厛鏍¢獙褰撳墠鎸傝浇閰嶇疆鏄惁鍙繛閫氥��</div>
+ </div>
+ <ElButton :loading="draftTesting" @click="handleDraftValidate">鑽夌杩為�氭�ф祴璇�</ElButton>
+ </div>
+ <ElAlert v-if="draftValidateResult" class="mt-4" :type="draftValidateResult.healthStatus === 'HEALTHY' ? 'success' : 'error'" :closable="false">
+ <div class="space-y-1 text-sm">
+ <div>{{ draftValidateResult.message || '--' }}</div>
+ <div v-if="draftValidateResult.initElapsedMs !== undefined && draftValidateResult.initElapsedMs !== null">
+ 鍒濆鍖栬�楁椂 {{ draftValidateResult.initElapsedMs }} ms
+ </div>
+ <div v-if="draftValidateResult.testedAt">{{ draftValidateResult.testedAt }}</div>
+ </div>
+ </ElAlert>
+ </div>
+
+ <div class="mt-4 rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4">
+ <div class="text-sm font-semibold text-[var(--art-gray-900)]">杩愯鏃剁姸鎬�</div>
+ <div class="mt-4 grid gap-4 md:grid-cols-2">
+ <div class="rounded-xl bg-[var(--art-main-bg-color)] p-3">
+ <div class="text-xs text-[var(--art-gray-500)]">鍋ュ悍鐘舵��</div>
+ <div class="mt-2 text-sm text-[var(--art-gray-900)]">{{ form.healthStatus || 'NOT_TESTED' }}</div>
+ </div>
+ <div class="rounded-xl bg-[var(--art-main-bg-color)] p-3">
+ <div class="text-xs text-[var(--art-gray-500)]">鏈�杩戞祴璇曟椂闂�</div>
+ <div class="mt-2 text-sm text-[var(--art-gray-900)]">{{ form['lastTestTime$'] || '--' }}</div>
+ </div>
+ <div class="rounded-xl bg-[var(--art-main-bg-color)] p-3">
+ <div class="text-xs text-[var(--art-gray-500)]">鏈�杩戝垵濮嬪寲鑰楁椂</div>
+ <div class="mt-2 text-sm text-[var(--art-gray-900)]">{{ form.lastInitElapsedMs ?? '--' }}</div>
+ </div>
+ <div class="rounded-xl bg-[var(--art-main-bg-color)] p-3">
+ <div class="text-xs text-[var(--art-gray-500)]">鏈�杩戞洿鏂版椂闂�</div>
+ <div class="mt-2 text-sm text-[var(--art-gray-900)]">{{ form['updateTime$'] || '--' }}</div>
+ </div>
+ </div>
+ <div class="mt-4 rounded-xl bg-[var(--art-main-bg-color)] p-3">
+ <div class="text-xs text-[var(--art-gray-500)]">鏈�杩戞祴璇曚俊鎭�</div>
+ <div class="mt-2 whitespace-pre-wrap break-all text-sm text-[var(--art-gray-900)]">{{ form.lastTestMessage || '--' }}</div>
+ </div>
+ </div>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">{{ isReadonly ? '鍏抽棴' : '鍙栨秷' }}</ElButton>
+ <ElButton v-if="!isReadonly" type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { ElMessage } from 'element-plus'
+ import { fetchValidateAiMcpDraftConnectivity } from '@/api/ai-config'
+ import {
+ buildAiMcpMountDialogModel,
+ buildAiMcpMountSavePayload,
+ getAiMcpMountTransportOptions
+ } from '../aiMcpMountPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ mode: { type: String, default: 'create' },
+ mcpMountData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(buildAiMcpMountDialogModel())
+ const draftTesting = ref(false)
+ const draftValidateResult = ref(null)
+
+ const isReadonly = computed(() => props.mode === 'show')
+ const dialogTitle = computed(() => {
+ if (props.mode === 'edit') return '缂栬緫鎸傝浇'
+ if (props.mode === 'show') return '鎸傝浇璇︽儏'
+ return '鏂板缓鎸傝浇'
+ })
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }],
+ transportType: [{ required: true, message: '璇烽�夋嫨浼犺緭绫诲瀷', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => {
+ const items = [
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ悕绉�' }
+ },
+ {
+ label: '浼犺緭绫诲瀷',
+ key: 'transportType',
+ type: 'select',
+ props: {
+ options: getAiMcpMountTransportOptions(),
+ placeholder: '璇烽�夋嫨浼犺緭绫诲瀷'
+ }
+ }
+ ]
+
+ if (form.transportType === 'BUILTIN') {
+ items.push({
+ label: '鍐呯疆 MCP 缂栫爜',
+ key: 'builtinCode',
+ type: 'input',
+ span: 24,
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ唴缃� MCP 缂栫爜' }
+ })
+ }
+
+ if (form.transportType === 'SSE_HTTP') {
+ items.push(
+ {
+ label: '鏈嶅姟鍦板潃',
+ key: 'serverUrl',
+ type: 'input',
+ span: 24,
+ props: { clearable: true, placeholder: '璇疯緭鍏ユ湇鍔″湴鍧�' }
+ },
+ {
+ label: 'SSE 绔偣',
+ key: 'endpoint',
+ type: 'input',
+ props: { clearable: true, placeholder: '璇疯緭鍏� SSE 绔偣' }
+ },
+ {
+ label: '璇锋眰澶� JSON',
+ key: 'headersJson',
+ type: 'input',
+ span: 24,
+ props: { type: 'textarea', rows: 4, placeholder: '璇疯緭鍏ヨ姹傚ご JSON' }
+ }
+ )
+ }
+
+ if (form.transportType === 'STDIO') {
+ items.push(
+ {
+ label: '鍛戒护',
+ key: 'command',
+ type: 'input',
+ span: 24,
+ props: { clearable: true, placeholder: '璇疯緭鍏ュ懡浠�' }
+ },
+ {
+ label: '鍛戒护鍙傛暟 JSON',
+ key: 'argsJson',
+ type: 'input',
+ span: 24,
+ props: { type: 'textarea', rows: 4, placeholder: '璇疯緭鍏ュ懡浠ゅ弬鏁� JSON' }
+ },
+ {
+ label: '鐜鍙橀噺 JSON',
+ key: 'envJson',
+ type: 'input',
+ span: 24,
+ props: { type: 'textarea', rows: 4, placeholder: '璇疯緭鍏ョ幆澧冨彉閲� JSON' }
+ }
+ )
+ }
+
+ items.push(
+ {
+ label: '璇锋眰瓒呮椂(ms)',
+ key: 'requestTimeoutMs',
+ type: 'number',
+ props: { min: 1000, placeholder: '璇疯緭鍏ヨ姹傝秴鏃�' }
+ },
+ {
+ label: '鎺掑簭',
+ key: 'sort',
+ type: 'number',
+ props: { min: 0, placeholder: '璇疯緭鍏ユ帓搴�' }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '鍚敤', value: 1 },
+ { label: '鍋滅敤', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: { type: 'textarea', rows: 3, placeholder: '璇疯緭鍏ュ娉�' }
+ }
+ )
+ return items
+ })
+
+ function resetForm() {
+ Object.assign(form, buildAiMcpMountDialogModel())
+ draftValidateResult.value = null
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildAiMcpMountDialogModel(props.mcpMountData))
+ draftValidateResult.value = null
+ }
+
+ async function handleDraftValidate() {
+ draftTesting.value = true
+ try {
+ draftValidateResult.value = await fetchValidateAiMcpDraftConnectivity(buildAiMcpMountSavePayload(form))
+ ElMessage.success(draftValidateResult.value?.message || '鑽夌杩為�氭�ф祴璇曟垚鍔�')
+ } catch (error) {
+ draftValidateResult.value = {
+ healthStatus: 'UNHEALTHY',
+ message: error?.message || '鑽夌杩為�氭�ф祴璇曞け璐�'
+ }
+ ElMessage.error(draftValidateResult.value.message)
+ } finally {
+ draftTesting.value = false
+ }
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', buildAiMcpMountSavePayload(form))
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.mcpMountData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-tools-drawer.vue b/rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-tools-drawer.vue
new file mode 100644
index 0000000..ce571b0
--- /dev/null
+++ b/rsf-design/src/views/system/ai-mcp-mount/modules/ai-mcp-tools-drawer.vue
@@ -0,0 +1,190 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="MCP 宸ュ叿棰勮"
+ size="760px"
+ @update:model-value="handleVisibleChange"
+ >
+ <div class="space-y-4">
+ <div class="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4">
+ <div>
+ <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ mountName || '褰撳墠鎸傝浇' }}</div>
+ <div class="mt-1 text-xs text-[var(--art-gray-500)]">鍙瑙堝伐鍏峰垪琛紝骞跺宸ュ叿鍏ュ弬鍋氳仈璋冩祴璇曘��</div>
+ </div>
+ <ElSpace wrap>
+ <ElButton :loading="toolsLoading" @click="loadTools">鍒锋柊宸ュ叿</ElButton>
+ <ElButton :loading="connectivityLoading" @click="handleConnectivityTest">杩為�氭�ф祴璇�</ElButton>
+ </ElSpace>
+ </div>
+
+ <ElAlert v-if="connectivityResult" :type="connectivityResult.healthStatus === 'HEALTHY' ? 'success' : 'error'" :closable="false">
+ <div class="space-y-1 text-sm">
+ <div>{{ connectivityResult.message || '--' }}</div>
+ <div v-if="connectivityResult.initElapsedMs !== undefined && connectivityResult.initElapsedMs !== null">
+ 鍒濆鍖栬�楁椂 {{ connectivityResult.initElapsedMs }} ms
+ </div>
+ <div v-if="connectivityResult.testedAt">{{ connectivityResult.testedAt }}</div>
+ </div>
+ </ElAlert>
+
+ <ElSkeleton :loading="toolsLoading" animated :rows="8">
+ <ElEmpty v-if="!tools.length" description="鏆傛棤宸ュ叿淇℃伅" :image-size="100" />
+
+ <div v-else class="space-y-4">
+ <div
+ v-for="tool in tools"
+ :key="tool.name"
+ class="rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4"
+ >
+ <div class="flex flex-wrap items-start justify-between gap-3">
+ <div>
+ <div class="text-base font-semibold text-[var(--art-gray-900)]">{{ tool.name }}</div>
+ <div class="mt-1 text-sm text-[var(--art-gray-500)]">{{ tool.description || '--' }}</div>
+ </div>
+ <ElButton :loading="testingToolName === tool.name" @click="handleToolTest(tool.name)">宸ュ叿娴嬭瘯</ElButton>
+ </div>
+
+ <div class="mt-4 grid gap-4 md:grid-cols-2">
+ <div class="space-y-2">
+ <div class="text-xs text-[var(--art-gray-500)]">杈撳叆鍙傛暟 JSON</div>
+ <ElInput
+ v-model="toolInputs[tool.name]"
+ type="textarea"
+ :rows="8"
+ placeholder='璇疯緭鍏� JSON锛屼緥濡� {"taskCode":"TK001"}'
+ />
+ </div>
+ <div class="space-y-2">
+ <div class="text-xs text-[var(--art-gray-500)]">宸ュ叿杈撳嚭</div>
+ <ElInput
+ :model-value="toolOutputs[tool.name] || ''"
+ type="textarea"
+ :rows="8"
+ readonly
+ placeholder="宸ュ叿杈撳嚭浼氭樉绀哄湪杩欓噷"
+ />
+ </div>
+ </div>
+
+ <div v-if="tool.inputSchema" class="mt-4 rounded-xl bg-[var(--art-main-bg-color)] p-3">
+ <div class="text-xs text-[var(--art-gray-500)]">杈撳叆 Schema</div>
+ <pre class="mt-2 whitespace-pre-wrap break-all text-xs leading-6 text-[var(--art-gray-900)]">{{ formatSchema(tool.inputSchema) }}</pre>
+ </div>
+ </div>
+ </div>
+ </ElSkeleton>
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchPreviewAiMcpTools, fetchTestAiMcpConnectivity, fetchTestAiMcpTool } from '@/api/ai-config'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ mountId: { type: [Number, String], default: null },
+ mountName: { type: String, default: '' }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const tools = ref([])
+ const toolsLoading = ref(false)
+ const connectivityLoading = ref(false)
+ const connectivityResult = ref(null)
+ const toolInputs = reactive({})
+ const toolOutputs = reactive({})
+ const testingToolName = ref('')
+
+ function resetState() {
+ tools.value = []
+ connectivityResult.value = null
+ testingToolName.value = ''
+ Object.keys(toolInputs).forEach((key) => delete toolInputs[key])
+ Object.keys(toolOutputs).forEach((key) => delete toolOutputs[key])
+ }
+
+ function formatSchema(schema) {
+ try {
+ return JSON.stringify(JSON.parse(schema), null, 2)
+ } catch {
+ return schema
+ }
+ }
+
+ async function loadTools() {
+ if (!props.mountId) return
+ toolsLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(fetchPreviewAiMcpTools(props.mountId), [], {
+ timeoutMessage: '宸ュ叿鍒楄〃鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ tools.value = Array.isArray(response) ? response : []
+ } catch (error) {
+ tools.value = []
+ ElMessage.error(error?.message || '鑾峰彇宸ュ叿鍒楄〃澶辫触')
+ } finally {
+ toolsLoading.value = false
+ }
+ }
+
+ async function handleConnectivityTest() {
+ if (!props.mountId) return
+ connectivityLoading.value = true
+ try {
+ connectivityResult.value = await guardRequestWithMessage(fetchTestAiMcpConnectivity(props.mountId), null, {
+ timeoutMessage: '杩為�氭�ф祴璇曡秴鏃讹紝宸插仠姝㈢瓑寰�'
+ })
+ ElMessage.success(connectivityResult.value?.message || '杩為�氭�ф祴璇曟垚鍔�')
+ } catch (error) {
+ ElMessage.error(error?.message || '杩為�氭�ф祴璇曞け璐�')
+ } finally {
+ connectivityLoading.value = false
+ }
+ }
+
+ async function handleToolTest(toolName) {
+ if (!props.mountId) return
+ const inputJson = toolInputs[toolName]?.trim?.() || ''
+ if (!inputJson) {
+ ElMessage.warning('璇疯緭鍏ュ伐鍏锋祴璇曞叆鍙� JSON')
+ return
+ }
+ testingToolName.value = toolName
+ try {
+ const result = await guardRequestWithMessage(
+ fetchTestAiMcpTool(props.mountId, {
+ toolName,
+ inputJson
+ }),
+ null,
+ {
+ timeoutMessage: '宸ュ叿娴嬭瘯瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ toolOutputs[toolName] = result?.output || JSON.stringify(result || {}, null, 2)
+ ElMessage.success('宸ュ叿娴嬭瘯鎴愬姛')
+ } catch (error) {
+ toolOutputs[toolName] = error?.message || '宸ュ叿娴嬭瘯澶辫触'
+ ElMessage.error(error?.message || '宸ュ叿娴嬭瘯澶辫触')
+ } finally {
+ testingToolName.value = ''
+ }
+ }
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ void loadTools()
+ } else {
+ resetState()
+ }
+ }
+ )
+</script>
diff --git a/rsf-design/src/views/system/ai-observe/aiObservePage.helpers.js b/rsf-design/src/views/system/ai-observe/aiObservePage.helpers.js
new file mode 100644
index 0000000..e3a6320
--- /dev/null
+++ b/rsf-design/src/views/system/ai-observe/aiObservePage.helpers.js
@@ -0,0 +1,165 @@
+export const AI_OBSERVE_REPORT_TITLE = 'AI 瑙傛祴鎶ヨ〃'
+
+export function createAiObserveSearchState() {
+ return {
+ condition: '',
+ requestId: '',
+ promptCode: '',
+ userId: '',
+ status: ''
+ }
+}
+
+export function getAiObservePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildAiObserveSearchParams(params = {}) {
+ return {
+ condition: params.condition?.trim?.() || '',
+ requestId: params.requestId?.trim?.() || '',
+ promptCode: params.promptCode?.trim?.() || '',
+ userId: params.userId?.trim?.() || '',
+ status: params.status || ''
+ }
+}
+
+export function buildAiObservePageQueryParams(params = {}) {
+ const normalized = buildAiObserveSearchParams(params)
+ return {
+ current: Number(params.current || 1),
+ pageSize: Number(params.pageSize || params.size || 20),
+ condition: normalized.condition,
+ requestId: normalized.requestId,
+ promptCode: normalized.promptCode,
+ userId: normalized.userId,
+ status: normalized.status || null
+ }
+}
+
+export function getAiObserveStatusMeta(status) {
+ if (status === 'COMPLETED') {
+ return { text: '宸插畬鎴�', type: 'success' }
+ }
+ if (status === 'FAILED') {
+ return { text: '澶辫触', type: 'danger' }
+ }
+ if (status === 'ABORTED') {
+ return { text: '宸蹭腑姝�', type: 'warning' }
+ }
+ if (status === 'RUNNING') {
+ return { text: '鎵ц涓�', type: 'primary' }
+ }
+ return { text: status || '--', type: 'info' }
+}
+
+export function formatAiObserveLatency(value) {
+ const normalized = Number(value)
+ return Number.isFinite(normalized) ? `${normalized} ms` : '--'
+}
+
+export function normalizeAiObserveStats(stats = {}) {
+ return {
+ callCount: Number(stats.callCount || 0),
+ successCount: Number(stats.successCount || 0),
+ failureCount: Number(stats.failureCount || 0),
+ avgElapsedMs: Number(stats.avgElapsedMs || 0),
+ avgFirstTokenLatencyMs: Number(stats.avgFirstTokenLatencyMs || 0),
+ totalTokens: Number(stats.totalTokens || 0),
+ avgTotalTokens: Number(stats.avgTotalTokens || 0),
+ toolSuccessRate: Number(stats.toolSuccessRate || 0),
+ toolCallCount: Number(stats.toolCallCount || 0),
+ toolFailureCount: Number(stats.toolFailureCount || 0)
+ }
+}
+
+export function normalizeAiObserveRow(row = {}) {
+ const statusMeta = getAiObserveStatusMeta(row.status)
+ const userLabel = row.userLabel || row.userId$ || row.userName || (row.userId ?? '')
+ return {
+ ...row,
+ promptName: row.promptName || '',
+ promptCode: row.promptCode || '',
+ model: row.model || '',
+ requestId: row.requestId || '',
+ userLabel: String(userLabel || ''),
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ elapsedText: formatAiObserveLatency(row.elapsedMs),
+ totalTokens: row.totalTokens ?? null,
+ mountedMcpNames: row.mountedMcpNames || '',
+ errorMessage: row.errorMessage || '',
+ 'createTime$': row['createTime$'] || '',
+ 'updateTime$': row['updateTime$'] || ''
+ }
+}
+
+export function buildAiObserveDetail(detail = {}, fallback = {}) {
+ const merged = {
+ ...fallback,
+ ...detail
+ }
+ const statusMeta = getAiObserveStatusMeta(merged.status)
+ return {
+ requestId: merged.requestId || '',
+ sessionId: merged.sessionId ?? '',
+ promptName: merged.promptName || '',
+ promptCode: merged.promptCode || '',
+ model: merged.model || '',
+ userId: merged.userId ?? '',
+ status: merged.status || '',
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ errorCategory: merged.errorCategory || '',
+ errorStage: merged.errorStage || '',
+ errorMessage: merged.errorMessage || '',
+ mountedMcpNames: merged.mountedMcpNames || '',
+ configuredMcpCount: merged.configuredMcpCount ?? null,
+ mountedMcpCount: merged.mountedMcpCount ?? null,
+ toolCallCount: merged.toolCallCount ?? null,
+ toolSuccessCount: merged.toolSuccessCount ?? null,
+ toolFailureCount: merged.toolFailureCount ?? null,
+ elapsedMs: merged.elapsedMs ?? null,
+ elapsedText: formatAiObserveLatency(merged.elapsedMs),
+ firstTokenLatencyMs: merged.firstTokenLatencyMs ?? null,
+ firstTokenLatencyText: formatAiObserveLatency(merged.firstTokenLatencyMs),
+ promptTokens: merged.promptTokens ?? null,
+ completionTokens: merged.completionTokens ?? null,
+ totalTokens: merged.totalTokens ?? null,
+ createTimeText: merged['createTime$'] || '--',
+ updateTimeText: merged['updateTime$'] || '--',
+ mcpLogs: Array.isArray(merged.mcpLogs) ? merged.mcpLogs : []
+ }
+}
+
+export function getAiObserveReportColumns() {
+ return [
+ { prop: 'requestId', label: '璇锋眰ID' },
+ { prop: 'promptLabel', label: 'Prompt' },
+ { prop: 'model', label: '妯″瀷' },
+ { prop: 'userLabel', label: '鐢ㄦ埛' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'elapsedText', label: '鎬昏�楁椂(ms)' },
+ { prop: 'totalTokensText', label: '鎬� Tokens' },
+ { prop: 'createTimeText', label: '鍒涘缓鏃堕棿' }
+ ]
+}
+
+export function buildAiObservePrintRows(rows = []) {
+ return rows.map((item) => {
+ const normalized = normalizeAiObserveRow(item)
+ return {
+ requestId: normalized.requestId || '--',
+ promptLabel: [normalized.promptName || '--', normalized.promptCode || '--'].join(' / '),
+ model: normalized.model || '--',
+ userLabel: normalized.userLabel || '--',
+ statusText: normalized.statusText,
+ elapsedText: normalized.elapsedText,
+ totalTokensText: normalized.totalTokens ?? '--',
+ createTimeText: normalized['createTime$'] || '--'
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/ai-observe/aiObserveTable.columns.js b/rsf-design/src/views/system/ai-observe/aiObserveTable.columns.js
new file mode 100644
index 0000000..b8b3674
--- /dev/null
+++ b/rsf-design/src/views/system/ai-observe/aiObserveTable.columns.js
@@ -0,0 +1,78 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createAiObserveTableColumns({ handleView }) {
+ return [
+ { type: 'selection', width: 52, fixed: 'left' },
+ {
+ prop: 'requestId',
+ label: '璇锋眰ID',
+ minWidth: 210,
+ showOverflowTooltip: true,
+ formatter: (row) => row.requestId || '-'
+ },
+ {
+ prop: 'promptName',
+ label: 'Prompt',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => {
+ const name = row.promptName || '-'
+ const code = row.promptCode || '-'
+ return `${name} / ${code}`
+ }
+ },
+ {
+ prop: 'model',
+ label: '妯″瀷',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.model || '-'
+ },
+ {
+ prop: 'userLabel',
+ label: '鐢ㄦ埛',
+ width: 110,
+ formatter: (row) => row.userLabel || '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText)
+ },
+ {
+ prop: 'elapsedText',
+ label: '鎬昏�楁椂',
+ width: 110,
+ formatter: (row) => row.elapsedText || '--'
+ },
+ {
+ prop: 'totalTokens',
+ label: '鎬� Tokens',
+ width: 110,
+ formatter: (row) => row.totalTokens ?? '--'
+ },
+ {
+ prop: 'createTime$',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ formatter: (row) => row['createTime$'] || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 70,
+ align: 'right',
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', { class: 'flex justify-end' }, [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ })
+ ])
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/ai-observe/index.vue b/rsf-design/src/views/system/ai-observe/index.vue
new file mode 100644
index 0000000..5833dbe
--- /dev/null
+++ b/rsf-design/src/views/system/ai-observe/index.vue
@@ -0,0 +1,279 @@
+<template>
+ <div class="ai-observe-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <div class="mb-5 grid gap-5 md:grid-cols-2 xl:grid-cols-4">
+ <ElCard
+ v-for="item in summaryCards"
+ :key="item.label"
+ class="rounded-3xl border border-[var(--art-border-color)] shadow-[0_12px_36px_rgba(15,23,42,0.04)]"
+ v-loading="statsLoading"
+ >
+ <div class="flex items-start justify-between gap-4">
+ <div class="min-w-0">
+ <div class="text-sm text-[var(--art-gray-500)]">{{ item.label }}</div>
+ <div class="mt-3 text-3xl font-semibold text-[var(--art-gray-900)]">{{ item.value }}</div>
+ <div class="mt-2 text-xs text-[var(--art-gray-500)]">{{ item.description }}</div>
+ </div>
+ <div class="flex size-12 shrink-0 items-center justify-center rounded-2xl" :class="item.iconWrapClass">
+ <ArtSvgIcon :icon="item.icon" class="text-xl" :class="item.iconClass" />
+ </div>
+ </div>
+ </ElCard>
+ </div>
+
+ <ElCard class="art-table-card">
+ <div class="mb-4">
+ <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">AI 瑙傛祴鎽樿</h3>
+ <p class="mt-1 text-sm text-[var(--art-gray-500)]">瑙傚療 AI 璋冪敤鐘舵�併�佽�楁椂銆乀okens 涓� MCP 宸ュ叿鎵ц鎯呭喌銆�</p>
+ </div>
+
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshAll">
+ <template #left>
+ <ElSpace wrap>
+ <ElTag effect="plain" type="info">鏈�杩戝叡 {{ pagination.total || 0 }} 鏉¤皟鐢ㄨ褰�</ElTag>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <AiObserveDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchAiCallLogPage,
+ fetchGetAiCallLogDetail,
+ fetchGetAiCallLogMcpLogs,
+ fetchGetAiObserveStats
+ } from '@/api/ai-config'
+ import {
+ AI_OBSERVE_REPORT_TITLE,
+ buildAiObserveDetail,
+ buildAiObservePageQueryParams,
+ buildAiObserveSearchParams,
+ createAiObserveSearchState,
+ getAiObservePaginationKey,
+ normalizeAiObserveRow,
+ normalizeAiObserveStats
+ } from './aiObservePage.helpers'
+ import { createAiObserveTableColumns } from './aiObserveTable.columns'
+ import AiObserveDetailDrawer from './modules/ai-observe-detail-drawer.vue'
+
+ defineOptions({ name: 'AiObserve' })
+
+ const searchForm = ref(createAiObserveSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const statsLoading = ref(false)
+ const stats = ref(normalizeAiObserveStats())
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ姹侷D鎴� Prompt'
+ }
+ },
+ {
+ label: '璇锋眰ID',
+ key: 'requestId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ姹侷D'
+ }
+ },
+ {
+ label: 'Prompt 缂栫爜',
+ key: 'promptCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� Prompt 缂栫爜'
+ }
+ },
+ {
+ label: '鐢ㄦ埛',
+ key: 'userId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ敤鎴稩D'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鎵ц涓�', value: 'RUNNING' },
+ { label: '宸插畬鎴�', value: 'COMPLETED' },
+ { label: '澶辫触', value: 'FAILED' },
+ { label: '宸蹭腑姝�', value: 'ABORTED' }
+ ]
+ }
+ }
+ ])
+
+ const summaryCards = computed(() => [
+ {
+ label: '璋冪敤娆℃暟',
+ value: stats.value.callCount,
+ description: `鎴愬姛 ${stats.value.successCount} / 澶辫触 ${stats.value.failureCount}`,
+ icon: 'ri:pulse-line',
+ iconWrapClass: 'bg-sky-50',
+ iconClass: 'text-sky-600'
+ },
+ {
+ label: '骞冲潎鑰楁椂',
+ value: `${stats.value.avgElapsedMs} ms`,
+ description: `棣栧寘 ${stats.value.avgFirstTokenLatencyMs} ms`,
+ icon: 'ri:timer-flash-line',
+ iconWrapClass: 'bg-amber-50',
+ iconClass: 'text-amber-600'
+ },
+ {
+ label: '鎬� Tokens',
+ value: stats.value.totalTokens,
+ description: `骞冲潎 ${stats.value.avgTotalTokens.toFixed(1)} Tokens`,
+ icon: 'ri:coin-line',
+ iconWrapClass: 'bg-emerald-50',
+ iconClass: 'text-emerald-600'
+ },
+ {
+ label: '宸ュ叿鎴愬姛鐜�',
+ value: `${stats.value.toolSuccessRate.toFixed(2)}%`,
+ description: `璋冪敤 ${stats.value.toolCallCount} / 澶辫触 ${stats.value.toolFailureCount}`,
+ icon: 'ri:service-line',
+ iconWrapClass: 'bg-violet-50',
+ iconClass: 'text-violet-600'
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const [detail, mcpLogs] = await Promise.all([
+ guardRequestWithMessage(fetchGetAiCallLogDetail(row.id), null, {
+ timeoutMessage: 'AI 瑙傛祴璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }),
+ guardRequestWithMessage(fetchGetAiCallLogMcpLogs(row.id), [], {
+ timeoutMessage: 'MCP 璋冪敤鏃ュ織鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ ])
+ detailData.value = buildAiObserveDetail(
+ {
+ ...detail,
+ mcpLogs: Array.isArray(mcpLogs) ? mcpLogs : []
+ },
+ row
+ )
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇 AI 瑙傛祴璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchAiCallLogPage,
+ apiParams: buildAiObservePageQueryParams(searchForm.value),
+ paginationKey: getAiObservePaginationKey(),
+ columnsFactory: () =>
+ createAiObserveTableColumns({
+ handleView: openDetail
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeAiObserveRow(item))
+ }
+ }
+ })
+
+ async function loadStats() {
+ statsLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(fetchGetAiObserveStats(), normalizeAiObserveStats(), {
+ timeoutMessage: 'AI 瑙傛祴鎽樿鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ stats.value = normalizeAiObserveStats(response)
+ } catch (error) {
+ stats.value = normalizeAiObserveStats()
+ ElMessage.error(error?.message || '鑾峰彇 AI 瑙傛祴鎽樿澶辫触')
+ } finally {
+ statsLoading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildAiObserveSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createAiObserveSearchState())
+ resetSearchParams()
+ }
+
+ async function refreshAll() {
+ await Promise.all([refreshData(), loadStats()])
+ }
+
+ void loadStats()
+</script>
diff --git a/rsf-design/src/views/system/ai-observe/modules/ai-observe-detail-drawer.vue b/rsf-design/src/views/system/ai-observe/modules/ai-observe-detail-drawer.vue
new file mode 100644
index 0000000..c1ed6fd
--- /dev/null
+++ b/rsf-design/src/views/system/ai-observe/modules/ai-observe-detail-drawer.vue
@@ -0,0 +1,110 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="AI 瑙傛祴璇︽儏"
+ size="720px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="14">
+ <div class="space-y-5">
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="璇锋眰ID">{{ displayData.requestId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浼氳瘽ID">{{ displayData.sessionId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="Prompt">{{ displayPrompt }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="妯″瀷">{{ displayData.model || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐢ㄦ埛">{{ displayData.userId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鎸傝浇 MCP">{{ displayData.mountedMcpNames || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閰嶇疆 MCP 鏁�">{{ displayData.configuredMcpCount ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="宸ュ叿璋冪敤">{{ displayData.toolCallCount ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎴愬姛/澶辫触">
+ {{ displayData.toolSuccessCount ?? '--' }} / {{ displayData.toolFailureCount ?? '--' }}
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鎬昏�楁椂">{{ displayData.elapsedText }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="棣栧寘鑰楁椂">{{ displayData.firstTokenLatencyText }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="Prompt Tokens">{{ displayData.promptTokens ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="Completion Tokens">{{ displayData.completionTokens ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="Total Tokens">{{ displayData.totalTokens ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿欒鍒嗙被">{{ displayData.errorCategory || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿欒闃舵">{{ displayData.errorStage || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElAlert v-if="displayData.errorMessage" type="error" :closable="false" show-icon>
+ <template #title>閿欒淇℃伅</template>
+ <div class="whitespace-pre-wrap break-all text-sm">{{ displayData.errorMessage }}</div>
+ </ElAlert>
+
+ <div class="space-y-3">
+ <div class="flex items-center justify-between">
+ <h4 class="text-base font-semibold text-[var(--art-gray-900)]">MCP 璋冪敤鏃ュ織</h4>
+ <span class="text-sm text-[var(--art-gray-500)]">{{ displayData.mcpLogs.length }} 鏉�</span>
+ </div>
+
+ <ElEmpty v-if="!displayData.mcpLogs.length" description="鏆傛棤 MCP 璋冪敤鏃ュ織" :image-size="100" />
+
+ <div v-else class="space-y-3">
+ <div
+ v-for="item in displayData.mcpLogs"
+ :key="item.id"
+ class="rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4"
+ >
+ <div class="flex flex-wrap items-start justify-between gap-3">
+ <div class="space-y-1">
+ <div class="text-sm font-semibold text-[var(--art-gray-900)]">{{ item.toolName || '--' }}</div>
+ <div class="text-xs text-[var(--art-gray-500)]">
+ {{ item.mountName || '--' }} 路 {{ item['createTime$'] || '--' }}
+ </div>
+ </div>
+ <ElTag :type="item.status === 'COMPLETED' ? 'success' : 'danger'" effect="light">
+ {{ item.status || '--' }}
+ </ElTag>
+ </div>
+
+ <div class="mt-4 grid gap-3 md:grid-cols-2">
+ <div class="rounded-xl bg-[var(--art-main-bg-color)] p-3">
+ <div class="text-xs text-[var(--art-gray-500)]">杈撳叆鎽樿</div>
+ <div class="mt-2 whitespace-pre-wrap break-all text-sm text-[var(--art-gray-900)]">
+ {{ item.inputSummary || '--' }}
+ </div>
+ </div>
+ <div class="rounded-xl bg-[var(--art-main-bg-color)] p-3">
+ <div class="text-xs text-[var(--art-gray-500)]">杈撳嚭鎽樿</div>
+ <div class="mt-2 whitespace-pre-wrap break-all text-sm text-[var(--art-gray-900)]">
+ {{ item.outputSummary || item.errorMessage || '--' }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { buildAiObserveDetail } from '../aiObservePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const displayData = computed(() => buildAiObserveDetail(props.detailData))
+ const displayPrompt = computed(() => {
+ const name = displayData.value.promptName || '--'
+ const code = displayData.value.promptCode || '--'
+ return `${name} / ${code}`
+ })
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/ai-param/aiParamPage.helpers.js b/rsf-design/src/views/system/ai-param/aiParamPage.helpers.js
new file mode 100644
index 0000000..f04e148
--- /dev/null
+++ b/rsf-design/src/views/system/ai-param/aiParamPage.helpers.js
@@ -0,0 +1,225 @@
+const AI_PARAM_REPORT_TITLE = 'AI 鍙傛暟鎶ヨ〃'
+
+const PROVIDER_OPTIONS = [{ label: 'OPENAI_COMPATIBLE', value: 'OPENAI_COMPATIBLE' }]
+
+const STATUS_OPTIONS = [
+ { label: '榛樿', value: 1 },
+ { label: '鍊欓��', value: 0 }
+]
+
+const VALIDATE_STATUS_META = {
+ VALID: { text: '宸叉牎楠�', type: 'success' },
+ INVALID: { text: '鏍¢獙澶辫触', type: 'danger' },
+ NOT_TESTED: { text: '鏈牎楠�', type: 'info' }
+}
+
+function createAiParamSearchState() {
+ return {
+ condition: '',
+ providerType: '',
+ model: '',
+ status: ''
+ }
+}
+
+function createAiParamFormState() {
+ return {
+ id: null,
+ name: '',
+ providerType: 'OPENAI_COMPATIBLE',
+ baseUrl: '',
+ apiKey: '',
+ model: '',
+ temperature: 0.7,
+ topP: 1,
+ maxTokens: null,
+ timeoutMs: 60000,
+ streamingEnabled: true,
+ status: 1,
+ memo: '',
+ validateStatus: 'NOT_TESTED',
+ lastValidateMessage: '',
+ lastValidateElapsedMs: null,
+ 'lastValidateTime$': '',
+ updateBy: '',
+ 'updateTime$': ''
+ }
+}
+
+function getAiParamPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+function getAiParamProviderOptions() {
+ return PROVIDER_OPTIONS
+}
+
+function getAiParamStatusOptions() {
+ return STATUS_OPTIONS
+}
+
+function getAiParamStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '榛樿', type: 'success' }
+ : { text: '鍊欓��', type: 'info' }
+}
+
+function getAiParamValidateStatusMeta(status) {
+ return VALIDATE_STATUS_META[String(status || '').trim()] || { text: status || '鏈煡', type: 'info' }
+}
+
+function buildAiParamSearchParams(params = {}) {
+ return {
+ condition: normalizeText(params.condition),
+ providerType: normalizeText(params.providerType),
+ model: normalizeText(params.model),
+ status: normalizeOptionalNumber(params.status)
+ }
+}
+
+function buildAiParamPageQueryParams(params = {}) {
+ return {
+ current: Number(params.current) > 0 ? Number(params.current) : 1,
+ pageSize: Number(params.pageSize) > 0 ? Number(params.pageSize) : 20,
+ condition: normalizeText(params.condition),
+ providerType: normalizeText(params.providerType),
+ model: normalizeText(params.model),
+ status: normalizeOptionalNumber(params.status)
+ }
+}
+
+function buildAiParamDialogModel(record = {}) {
+ const base = createAiParamFormState()
+ return {
+ ...base,
+ ...record,
+ id: normalizeOptionalNumber(record.id) ?? null,
+ providerType: normalizeText(record.providerType) || base.providerType,
+ status: normalizeOptionalNumber(record.status) ?? base.status,
+ temperature: normalizeOptionalFloat(record.temperature) ?? base.temperature,
+ topP: normalizeOptionalFloat(record.topP) ?? base.topP,
+ maxTokens: normalizeOptionalNumber(record.maxTokens),
+ timeoutMs: normalizeOptionalNumber(record.timeoutMs) ?? base.timeoutMs,
+ streamingEnabled:
+ record.streamingEnabled === undefined || record.streamingEnabled === null
+ ? base.streamingEnabled
+ : Boolean(record.streamingEnabled),
+ validateStatus: normalizeText(record.validateStatus) || base.validateStatus,
+ lastValidateMessage: normalizeText(record.lastValidateMessage),
+ lastValidateElapsedMs: normalizeOptionalNumber(record.lastValidateElapsedMs),
+ 'lastValidateTime$': normalizeText(record['lastValidateTime$']),
+ updateBy: record.updateBy ?? '',
+ 'updateTime$': normalizeText(record['updateTime$'])
+ }
+}
+
+function buildAiParamSavePayload(formData = {}) {
+ return {
+ ...(normalizeOptionalNumber(formData.id) !== null ? { id: normalizeOptionalNumber(formData.id) } : {}),
+ name: normalizeText(formData.name),
+ providerType: normalizeText(formData.providerType) || 'OPENAI_COMPATIBLE',
+ baseUrl: normalizeText(formData.baseUrl),
+ apiKey: normalizeText(formData.apiKey),
+ model: normalizeText(formData.model),
+ temperature: normalizeOptionalFloat(formData.temperature),
+ topP: normalizeOptionalFloat(formData.topP),
+ maxTokens: normalizeOptionalNumber(formData.maxTokens),
+ timeoutMs: normalizeOptionalNumber(formData.timeoutMs),
+ streamingEnabled: Boolean(formData.streamingEnabled),
+ status: normalizeOptionalNumber(formData.status) ?? 1,
+ memo: normalizeText(formData.memo)
+ }
+}
+
+function normalizeAiParamRow(record = {}) {
+ const statusMeta = getAiParamStatusMeta(record.status)
+ const validateMeta = getAiParamValidateStatusMeta(record.validateStatus)
+
+ return {
+ ...record,
+ providerType: normalizeText(record.providerType) || 'OPENAI_COMPATIBLE',
+ model: normalizeText(record.model),
+ baseUrl: normalizeText(record.baseUrl),
+ memo: normalizeText(record.memo),
+ statusBool: Number(record.status) === 1,
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ validateStatusText: validateMeta.text,
+ validateStatusType: validateMeta.type,
+ streamingEnabled: Boolean(record.streamingEnabled),
+ streamingText: record.streamingEnabled ? '娴佸紡鍝嶅簲' : '鏍囧噯鍝嶅簲',
+ 'createTime$': normalizeText(record['createTime$']),
+ 'updateTime$': normalizeText(record['updateTime$']),
+ 'lastValidateTime$': normalizeText(record['lastValidateTime$'])
+ }
+}
+
+function buildAiParamPrintRows(records = []) {
+ return records.map((item) => {
+ const row = normalizeAiParamRow(item)
+ return {
+ name: row.name || '-',
+ providerType: row.providerType || '-',
+ model: row.model || '-',
+ statusText: row.statusText,
+ validateStatusText: row.validateStatusText,
+ timeoutMs: row.timeoutMs ?? '--',
+ updateTime: row['updateTime$'] || '--',
+ memo: row.memo || '--'
+ }
+ })
+}
+
+function getAiParamReportColumns() {
+ return [
+ { label: '鍚嶇О', prop: 'name' },
+ { label: '鎻愪緵鏂�', prop: 'providerType' },
+ { label: '妯″瀷', prop: 'model' },
+ { label: '榛樿鐘舵��', prop: 'statusText' },
+ { label: '鏍¢獙鐘舵��', prop: 'validateStatusText' },
+ { label: '瓒呮椂鏃堕棿(ms)', prop: 'timeoutMs' },
+ { label: '鏇存柊鏃堕棿', prop: 'updateTime' },
+ { label: '澶囨敞', prop: 'memo' }
+ ]
+}
+
+function normalizeText(value) {
+ return value === undefined || value === null ? '' : String(value).trim()
+}
+
+function normalizeOptionalNumber(value) {
+ if (value === undefined || value === null || value === '') {
+ return null
+ }
+ const number = Number(value)
+ return Number.isFinite(number) ? number : null
+}
+
+function normalizeOptionalFloat(value) {
+ if (value === undefined || value === null || value === '') {
+ return null
+ }
+ const number = Number(value)
+ return Number.isFinite(number) ? number : null
+}
+
+export {
+ AI_PARAM_REPORT_TITLE,
+ buildAiParamDialogModel,
+ buildAiParamPageQueryParams,
+ buildAiParamPrintRows,
+ buildAiParamSavePayload,
+ buildAiParamSearchParams,
+ createAiParamFormState,
+ createAiParamSearchState,
+ getAiParamPaginationKey,
+ getAiParamProviderOptions,
+ getAiParamReportColumns,
+ getAiParamStatusMeta,
+ getAiParamStatusOptions,
+ getAiParamValidateStatusMeta,
+ normalizeAiParamRow
+}
diff --git a/rsf-design/src/views/system/ai-param/index.vue b/rsf-design/src/views/system/ai-param/index.vue
new file mode 100644
index 0000000..972e5ba
--- /dev/null
+++ b/rsf-design/src/views/system/ai-param/index.vue
@@ -0,0 +1,392 @@
+<template>
+ <div class="art-full-height ai-param-page">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <AiParamRuntimeSummary :key="summaryRefreshSeed" />
+
+ <ElCard class="art-table-card ai-param-list-card">
+ <div class="mb-5 flex flex-wrap items-center justify-between gap-4">
+ <div>
+ <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">AI 鍙傛暟</h3>
+ <p class="mt-1 text-sm text-[var(--art-gray-500)]">鎸夊崱鐗囩鐞嗗綋鍓嶇鎴风殑妯″瀷鎺ュ叆鍙傛暟涓庨粯璁ら厤缃��</p>
+ </div>
+
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="openCreateDialog" v-ripple>鏂板缓鍙傛暟</ElButton>
+ <ElButton :loading="exportLoading" @click="handleExport" v-ripple>瀵煎嚭</ElButton>
+ <ElButton :loading="loading" @click="refreshData" v-ripple>鍒锋柊</ElButton>
+ </ElSpace>
+ </div>
+
+ <div v-loading="loading">
+ <div v-if="data.length" class="grid gap-5 md:grid-cols-2 2xl:grid-cols-3">
+ <article
+ v-for="item in data"
+ :key="item.id"
+ class="overflow-hidden rounded-3xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-4 shadow-[0_10px_30px_rgba(15,23,42,0.04)]"
+ >
+ <div class="flex items-start justify-between gap-4">
+ <div class="min-w-0 flex-1">
+ <div class="flex items-center gap-3">
+ <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-sky-50 text-sky-600">
+ <ArtSvgIcon icon="ri:robot-2-line" class="text-xl" />
+ </div>
+ <div class="min-w-0">
+ <h4 class="truncate text-base font-semibold text-[var(--art-gray-900)]">{{ item.name || '--' }}</h4>
+ <p class="mt-1 truncate text-sm text-[var(--art-gray-500)]">{{ item.model || '--' }}</p>
+ </div>
+ </div>
+ </div>
+
+ <ElTag :type="item.statusType" effect="light">{{ item.statusText }}</ElTag>
+ </div>
+
+ <div class="mt-3 flex flex-wrap gap-2">
+ <ElTag size="small" effect="plain">{{ item.providerType }}</ElTag>
+ <ElTag size="small" :type="item.validateStatusType" effect="plain">
+ {{ item.validateStatusText }}
+ </ElTag>
+ <ElTag size="small" effect="plain">{{ item.streamingText }}</ElTag>
+ </div>
+
+ <div class="mt-4 grid gap-2 xl:grid-cols-2">
+ <div
+ class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2.5"
+ >
+ <p class="text-xs text-[var(--art-gray-500)]">鍩虹鍦板潃</p>
+ <p class="mt-1.5 break-all text-sm leading-6 text-[var(--art-gray-900)]">{{ item.baseUrl || '--' }}</p>
+ </div>
+ <div
+ class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2.5"
+ >
+ <p class="text-xs text-[var(--art-gray-500)]">鏈�杩戞牎楠�</p>
+ <p class="mt-1.5 text-sm text-[var(--art-gray-900)]">{{ item['lastValidateTime$'] || '鏈牎楠�' }}</p>
+ </div>
+ </div>
+
+ <div class="mt-4 grid grid-cols-2 gap-2 text-sm 2xl:grid-cols-4">
+ <div class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2">
+ <p class="text-xs text-[var(--art-gray-500)]">Temperature</p>
+ <p class="mt-1 text-sm font-medium text-[var(--art-gray-900)]">{{ item.temperature ?? '--' }}</p>
+ </div>
+ <div class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2">
+ <p class="text-xs text-[var(--art-gray-500)]">Top P</p>
+ <p class="mt-1 text-sm font-medium text-[var(--art-gray-900)]">{{ item.topP ?? '--' }}</p>
+ </div>
+ <div class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2">
+ <p class="text-xs text-[var(--art-gray-500)]">Max Tokens</p>
+ <p class="mt-1 text-sm font-medium text-[var(--art-gray-900)]">{{ item.maxTokens ?? '--' }}</p>
+ </div>
+ <div class="min-w-0 rounded-2xl bg-slate-50 px-3 py-2">
+ <p class="text-xs text-[var(--art-gray-500)]">瓒呮椂鏃堕棿</p>
+ <p class="mt-1 text-sm font-medium text-[var(--art-gray-900)]">{{ item.timeoutMs ?? '--' }} ms</p>
+ </div>
+ </div>
+
+ <div class="mt-4 rounded-2xl bg-amber-50/70 px-3 py-2.5">
+ <p class="text-xs text-[var(--art-gray-500)]">澶囨敞</p>
+ <p class="mt-1.5 line-clamp-2 text-sm leading-6 text-[var(--art-gray-900)]">{{ item.memo || '--' }}</p>
+ </div>
+
+ <div class="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-[var(--art-border-color)] pt-3">
+ <div class="flex items-center gap-2 text-xs text-[var(--art-gray-500)]">
+ <span>鏇存柊鏃堕棿</span>
+ <span>{{ item['updateTime$'] || '--' }}</span>
+ </div>
+
+ <ElSpace wrap>
+ <ElButton text @click="openDetailDialog(item)">璇︽儏</ElButton>
+ <ElButton v-auth="'edit'" text @click="openEditDialog(item)">缂栬緫</ElButton>
+ <ElButton
+ v-auth="'edit'"
+ text
+ :disabled="item.statusBool || defaultUpdatingId === item.id"
+ :loading="defaultUpdatingId === item.id"
+ @click="handleSetDefault(item)"
+ >
+ 璁句负榛樿
+ </ElButton>
+ <ElButton v-auth="'delete'" text type="danger" @click="handleDelete(item)">鍒犻櫎</ElButton>
+ </ElSpace>
+ </div>
+ </article>
+ </div>
+
+ <ElEmpty v-else description="鏆傛棤 AI 鍙傛暟鏁版嵁" :image-size="110" />
+ </div>
+
+ <div class="mt-6 flex justify-end">
+ <ElPagination
+ background
+ layout="total, sizes, prev, pager, next, jumper"
+ :current-page="pagination.current"
+ :page-size="pagination.size"
+ :total="pagination.total"
+ :page-sizes="[20, 50, 100]"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+
+ <AiParamDialog
+ v-model:visible="dialogVisible"
+ :mode="dialogMode"
+ :ai-param-data="currentAiParamData"
+ @submit="handleDialogSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchAiParamPage,
+ fetchDeleteAiParam,
+ fetchExportAiParamReport,
+ fetchGetAiParamDetail,
+ fetchSaveAiParam,
+ fetchSetAiParamDefault,
+ fetchUpdateAiParam
+ } from '@/api/ai-config'
+ import AiParamDialog from './modules/ai-param-dialog.vue'
+ import AiParamRuntimeSummary from './modules/ai-param-runtime-summary.vue'
+ import {
+ buildAiParamDialogModel,
+ buildAiParamPageQueryParams,
+ buildAiParamSavePayload,
+ buildAiParamSearchParams,
+ createAiParamSearchState,
+ getAiParamPaginationKey,
+ normalizeAiParamRow
+ } from './aiParamPage.helpers'
+
+ defineOptions({ name: 'AiParam' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createAiParamSearchState())
+ const dialogVisible = ref(false)
+ const dialogMode = ref('create')
+ const currentAiParamData = ref(buildAiParamDialogModel())
+ const defaultUpdatingId = ref(null)
+ const exportLoading = ref(false)
+ const summaryRefreshSeed = ref(0)
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ弬鏁板悕绉�'
+ }
+ },
+ {
+ label: '鎻愪緵鏂�',
+ key: 'providerType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ彁渚涙柟绫诲瀷'
+ }
+ },
+ {
+ label: '妯″瀷',
+ key: 'model',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユā鍨嬪悕绉�'
+ }
+ },
+ {
+ label: '榛樿鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '榛樿', value: 1 },
+ { label: '鍊欓��', value: 0 }
+ ]
+ }
+ }
+ ])
+
+ const {
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchAiParamPage,
+ apiParams: buildAiParamPageQueryParams(searchForm.value),
+ paginationKey: getAiParamPaginationKey()
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeAiParamRow(item))
+ }
+ }
+ })
+
+ async function openEditDialog(record) {
+ try {
+ currentAiParamData.value = buildAiParamDialogModel(await fetchGetAiParamDetail(record.id))
+ dialogMode.value = 'edit'
+ dialogVisible.value = true
+ } catch {
+ return
+ }
+ }
+
+ async function openDetailDialog(record) {
+ try {
+ currentAiParamData.value = buildAiParamDialogModel(await fetchGetAiParamDetail(record.id))
+ dialogMode.value = 'show'
+ dialogVisible.value = true
+ } catch {
+ return
+ }
+ }
+
+ function openCreateDialog() {
+ currentAiParamData.value = buildAiParamDialogModel()
+ dialogMode.value = 'create'
+ dialogVisible.value = true
+ }
+
+ async function handleDialogSubmit(payload) {
+ try {
+ if (dialogMode.value === 'edit') {
+ await fetchUpdateAiParam(buildAiParamSavePayload(payload))
+ ElMessage.success('淇敼鎴愬姛')
+ dialogVisible.value = false
+ summaryRefreshSeed.value += 1
+ await refreshUpdate()
+ return
+ }
+ await fetchSaveAiParam(buildAiParamSavePayload(payload))
+ ElMessage.success('鏂板鎴愬姛')
+ dialogVisible.value = false
+ summaryRefreshSeed.value += 1
+ await refreshCreate()
+ } catch {
+ return
+ }
+ }
+
+ async function handleSetDefault(record) {
+ if (!record?.id || record.statusBool) return
+ defaultUpdatingId.value = record.id
+ try {
+ await fetchSetAiParamDefault(record.id)
+ ElMessage.success('榛樿鍙傛暟宸插垏鎹�')
+ summaryRefreshSeed.value += 1
+ await refreshUpdate()
+ } catch {
+ return
+ } finally {
+ defaultUpdatingId.value = null
+ }
+ }
+
+ async function handleDelete(record) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄� AI 鍙傛暟銆�${record?.name || record?.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteAiParam(record.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshRemove()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ async function handleExport() {
+ exportLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchExportAiParamReport(buildAiParamSearchParams(searchForm.value), {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ null,
+ {
+ timeoutMessage: '瀵煎嚭璇锋眰瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ if (!response) return
+ if (!response.ok) {
+ throw new Error(`瀵煎嚭澶辫触 (${response.status})`)
+ }
+ const blob = await response.blob()
+ const url = window.URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = 'ai-param.xlsx'
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ window.URL.revokeObjectURL(url)
+ ElMessage.success('瀵煎嚭鎴愬姛')
+ } catch (error) {
+ ElMessage.error(error?.message || '瀵煎嚭澶辫触')
+ } finally {
+ exportLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildAiParamSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createAiParamSearchState())
+ resetSearchParams()
+ }
+</script>
+
+<style scoped>
+ .ai-param-page {
+ overflow-y: auto;
+ }
+
+ .ai-param-list-card {
+ flex: none;
+ }
+
+ .ai-param-list-card :deep(.el-card__body) {
+ height: auto;
+ overflow: visible;
+ }
+</style>
diff --git a/rsf-design/src/views/system/ai-param/modules/ai-param-dialog.vue b/rsf-design/src/views/system/ai-param/modules/ai-param-dialog.vue
new file mode 100644
index 0000000..e48a405
--- /dev/null
+++ b/rsf-design/src/views/system/ai-param/modules/ai-param-dialog.vue
@@ -0,0 +1,325 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="920px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <div v-if="showRuntimeSection" class="mt-2 rounded-2xl border border-[var(--art-border-color)] px-5 py-4">
+ <div class="mb-3 flex items-center justify-between gap-4">
+ <div>
+ <h4 class="text-base font-semibold text-[var(--art-gray-900)]">杩愯鏃剁姸鎬�</h4>
+ <p class="mt-1 text-sm text-[var(--art-gray-500)]">淇濆瓨鍓嶅彲鍏堟墽琛岃崏绋挎牎楠岋紝杩愯鏃剁姸鎬佺敱鍚庣鐪熷疄杩斿洖銆�</p>
+ </div>
+ <ElButton v-if="!isReadonly" :loading="validateLoading" @click="handleValidateDraft">
+ 鑽夌鏍¢獙
+ </ElButton>
+ </div>
+
+ <ElAlert
+ v-if="validateResultMessage"
+ class="!mb-4"
+ :type="validateAlertType"
+ :closable="false"
+ :title="validateResultMessage"
+ />
+
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="鏍¢獙鐘舵��">{{ form.validateStatus || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�杩戞牎楠岃�楁椂">
+ {{ form.lastValidateElapsedMs !== null && form.lastValidateElapsedMs !== undefined ? `${form.lastValidateElapsedMs} ms` : '--' }}
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�杩戞牎楠屾椂闂�">{{ form['lastValidateTime$'] || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�杩戞洿鏂颁汉">{{ form.updateBy || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�杩戞洿鏂版椂闂�" :span="2">{{ form['updateTime$'] || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�杩戞牎楠屼俊鎭�" :span="2">
+ <div class="whitespace-pre-wrap break-all text-sm leading-6 text-[var(--art-gray-700)]">
+ {{ form.lastValidateMessage || '--' }}
+ </div>
+ </ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">{{ isReadonly ? '鍏抽棴' : '鍙栨秷' }}</ElButton>
+ <ElButton v-if="!isReadonly" type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { fetchValidateAiParamDraft } from '@/api/ai-config'
+ import {
+ buildAiParamDialogModel,
+ buildAiParamSavePayload,
+ createAiParamFormState,
+ getAiParamProviderOptions,
+ getAiParamStatusOptions
+ } from '../aiParamPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ mode: { type: String, default: 'create' },
+ aiParamData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['submit', 'update:visible'])
+ const formRef = ref()
+ const form = reactive(createAiParamFormState())
+ const validateLoading = ref(false)
+ const validateResult = ref(null)
+
+ const isReadonly = computed(() => props.mode === 'show')
+ const showRuntimeSection = computed(() => Boolean(form.id) || props.mode !== 'create')
+ const dialogTitle = computed(() => {
+ if (props.mode === 'edit') return '缂栬緫 AI 鍙傛暟'
+ if (props.mode === 'show') return 'AI 鍙傛暟璇︽儏'
+ return '鏂板缓 AI 鍙傛暟'
+ })
+
+ const validateAlertType = computed(() =>
+ validateResult.value?.status === 'VALID' ? 'success' : 'warning'
+ )
+
+ const validateResultMessage = computed(() => {
+ if (!validateResult.value?.message) {
+ return ''
+ }
+ const suffix = [
+ validateResult.value.elapsedMs ? `${validateResult.value.elapsedMs} ms` : '',
+ validateResult.value.validatedAt || ''
+ ]
+ .filter(Boolean)
+ .join(' 路 ')
+ return suffix ? `${validateResult.value.message} 路 ${suffix}` : validateResult.value.message
+ })
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ュ弬鏁板悕绉�', trigger: 'blur' }],
+ providerType: [{ required: true, message: '璇烽�夋嫨鎻愪緵鏂圭被鍨�', trigger: 'change' }],
+ baseUrl: [{ required: true, message: '璇疯緭鍏ュ熀纭�鍦板潃', trigger: 'blur' }],
+ apiKey: [{ required: true, message: '璇疯緭鍏� API Key', trigger: 'blur' }],
+ model: [{ required: true, message: '璇疯緭鍏ユā鍨嬪悕绉�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '鍙傛暟鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ弬鏁板悕绉�',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '鎻愪緵鏂圭被鍨�',
+ key: 'providerType',
+ type: 'select',
+ props: {
+ options: getAiParamProviderOptions(),
+ disabled: isReadonly.value,
+ placeholder: '璇烽�夋嫨鎻愪緵鏂圭被鍨�'
+ }
+ },
+ {
+ label: '鍩虹鍦板潃',
+ key: 'baseUrl',
+ type: 'input',
+ span: 24,
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ吋瀹� OpenAI 鐨勫熀纭�鍦板潃',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: 'API Key',
+ key: 'apiKey',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� API Key',
+ disabled: isReadonly.value,
+ type: isReadonly.value ? 'text' : 'password',
+ showPassword: !isReadonly.value
+ }
+ },
+ {
+ label: '妯″瀷鍚嶇О',
+ key: 'model',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユā鍨嬪悕绉�',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: 'Temperature',
+ key: 'temperature',
+ type: 'number',
+ props: {
+ min: 0,
+ max: 2,
+ step: 0.1,
+ precision: 2,
+ placeholder: '璇疯緭鍏� temperature',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: 'Top P',
+ key: 'topP',
+ type: 'number',
+ props: {
+ min: 0,
+ max: 1,
+ step: 0.1,
+ precision: 2,
+ placeholder: '璇疯緭鍏� topP',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '鏈�澶� Token',
+ key: 'maxTokens',
+ type: 'number',
+ props: {
+ min: 1,
+ step: 1,
+ placeholder: '璇疯緭鍏ユ渶澶� token',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '瓒呮椂鏃堕棿(ms)',
+ key: 'timeoutMs',
+ type: 'number',
+ props: {
+ min: 1000,
+ step: 1000,
+ placeholder: '璇疯緭鍏ヨ秴鏃舵椂闂�',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '娴佸紡鍝嶅簲',
+ key: 'streamingEnabled',
+ type: 'switch',
+ props: {
+ disabled: isReadonly.value,
+ activeText: '寮�鍚�',
+ inactiveText: '鍏抽棴'
+ }
+ },
+ {
+ label: '榛樿鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ disabled: isReadonly.value,
+ options: getAiParamStatusOptions(),
+ placeholder: '璇烽�夋嫨榛樿鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ disabled: isReadonly.value,
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createAiParamFormState())
+ validateResult.value = null
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildAiParamDialogModel(props.aiParamData))
+ validateResult.value = null
+ }
+
+ async function handleValidateDraft() {
+ validateLoading.value = true
+ try {
+ const result = await fetchValidateAiParamDraft(buildAiParamSavePayload(form))
+ validateResult.value = result
+ Object.assign(form, {
+ validateStatus: result?.status || form.validateStatus,
+ lastValidateMessage: result?.message || '',
+ lastValidateElapsedMs: result?.elapsedMs ?? null,
+ 'lastValidateTime$': result?.validatedAt || ''
+ })
+ } catch {
+ return
+ } finally {
+ validateLoading.value = false
+ }
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', buildAiParamSavePayload(form))
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.aiParamData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/ai-param/modules/ai-param-runtime-summary.vue b/rsf-design/src/views/system/ai-param/modules/ai-param-runtime-summary.vue
new file mode 100644
index 0000000..cf200ff
--- /dev/null
+++ b/rsf-design/src/views/system/ai-param/modules/ai-param-runtime-summary.vue
@@ -0,0 +1,142 @@
+<template>
+ <ElCard class="art-table-card ai-param-runtime-summary-card !mb-5" shadow="never">
+ <div class="mb-3 flex items-start justify-between gap-4">
+ <div>
+ <h3 class="text-base font-semibold text-[var(--art-gray-900)]">杩愯鏃舵憳瑕�</h3>
+ <p class="mt-0.5 text-xs text-[var(--art-gray-500)]">褰撳墠鐢熸晥鐨勬ā鍨嬨�丳rompt 涓� MCP 鎸傝浇姒傚喌</p>
+ </div>
+ <ElButton text :loading="loading" @click="loadSummary">鍒锋柊鎽樿</ElButton>
+ </div>
+
+ <ElAlert
+ v-if="errorMessage"
+ type="warning"
+ :closable="false"
+ class="!mb-5"
+ :title="errorMessage"
+ />
+
+ <div class="grid gap-2.5 lg:grid-cols-3" v-loading="loading">
+ <div class="rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-3 py-3">
+ <div class="flex items-center gap-2.5">
+ <div class="flex size-8 items-center justify-center rounded-xl bg-sky-50 text-sky-600">
+ <ArtSvgIcon icon="ri:robot-2-line" class="text-base" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <p class="text-[11px] text-[var(--art-gray-500)]">褰撳墠妯″瀷</p>
+ <h4 class="truncate text-sm font-semibold text-[var(--art-gray-900)]">
+ {{ summary.activeModel || '--' }}
+ </h4>
+ </div>
+ </div>
+ <div class="mt-2 flex items-center justify-between gap-3">
+ <p class="truncate text-xs text-[var(--art-gray-700)]">{{ summary.activeParamName || '--' }}</p>
+ <ElTag size="small" :type="validateMeta.type" effect="light">
+ {{ validateMeta.text }}
+ </ElTag>
+ </div>
+ <p class="mt-1 text-[11px] text-[var(--art-gray-500)]">{{ summary.activeParamValidatedAt || '鏈牎楠�' }}</p>
+ </div>
+
+ <div class="rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-3 py-3">
+ <div class="flex items-center gap-2.5">
+ <div class="flex size-8 items-center justify-center rounded-xl bg-violet-50 text-violet-600">
+ <ArtSvgIcon icon="ri:lightbulb-flash-line" class="text-base" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <p class="text-[11px] text-[var(--art-gray-500)]">褰撳墠 Prompt</p>
+ <h4 class="truncate text-sm font-semibold text-[var(--art-gray-900)]">
+ {{ summary.promptName || '--' }}
+ </h4>
+ </div>
+ </div>
+ <p class="mt-2 truncate text-xs text-[var(--art-gray-700)]">
+ {{ [summary.promptCode, summary.promptScene].filter(Boolean).join(' / ') || '--' }}
+ </p>
+ <p class="mt-1 text-[11px] text-[var(--art-gray-500)]">
+ 鏈�杩戞洿鏂版椂闂� {{ summary.activePromptUpdatedAt || '--' }}
+ </p>
+ </div>
+
+ <div class="rounded-2xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-3 py-3">
+ <div class="flex items-center gap-2.5">
+ <div class="flex size-8 items-center justify-center rounded-xl bg-emerald-50 text-emerald-600">
+ <ArtSvgIcon icon="ri:plug-2-line" class="text-base" />
+ </div>
+ <div class="min-w-0 flex-1">
+ <p class="text-[11px] text-[var(--art-gray-500)]">宸插惎鐢� MCP</p>
+ <h4 class="text-sm font-semibold text-[var(--art-gray-900)]">
+ {{ summary.enabledMcpCount ?? 0 }} 涓�
+ </h4>
+ </div>
+ </div>
+ <div class="mt-2 flex flex-wrap gap-1.5">
+ <ElTag
+ v-for="name in enabledMcpNames"
+ :key="name"
+ size="small"
+ effect="plain"
+ >
+ {{ name }}
+ </ElTag>
+ <span v-if="!enabledMcpNames.length" class="text-xs text-[var(--art-gray-500)]">鏆傛棤鎸傝浇</span>
+ </div>
+ </div>
+ </div>
+ </ElCard>
+</template>
+
+<script setup>
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchGetAiConfigSummary } from '@/api/ai-config'
+ import { getAiParamValidateStatusMeta } from '../aiParamPage.helpers'
+
+ const props = defineProps({
+ promptCode: { type: String, default: '' }
+ })
+
+ const loading = ref(false)
+ const summary = ref({})
+ const errorMessage = ref('')
+
+ const validateMeta = computed(() =>
+ getAiParamValidateStatusMeta(summary.value?.activeParamValidateStatus)
+ )
+
+ const enabledMcpNames = computed(() =>
+ Array.isArray(summary.value?.enabledMcpNames) ? summary.value.enabledMcpNames : []
+ )
+
+ async function loadSummary() {
+ loading.value = true
+ errorMessage.value = ''
+ const data = await guardRequestWithMessage(
+ fetchGetAiConfigSummary(props.promptCode),
+ null,
+ {
+ timeoutMessage: '杩愯鏃舵憳瑕佸姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
+ loading.value = false
+ if (!data) {
+ errorMessage.value = '杩愯鏃舵憳瑕佹殏鏃朵笉鍙敤'
+ summary.value = {}
+ return
+ }
+ summary.value = data
+ }
+
+ onMounted(() => {
+ loadSummary()
+ })
+</script>
+
+<style scoped>
+ .ai-param-runtime-summary-card {
+ flex: none;
+ }
+
+ .ai-param-runtime-summary-card :deep(.el-card__body) {
+ height: auto;
+ }
+</style>
diff --git a/rsf-design/src/views/system/ai-prompt/aiPromptPage.helpers.js b/rsf-design/src/views/system/ai-prompt/aiPromptPage.helpers.js
new file mode 100644
index 0000000..5f22a4c
--- /dev/null
+++ b/rsf-design/src/views/system/ai-prompt/aiPromptPage.helpers.js
@@ -0,0 +1,182 @@
+const AI_PROMPT_REPORT_TITLE = 'Prompt 绠$悊鎶ヨ〃'
+
+const STATUS_OPTIONS = [
+ { label: '鍚敤', value: 1 },
+ { label: '鍋滅敤', value: 0 }
+]
+
+function createAiPromptSearchState() {
+ return {
+ condition: '',
+ code: '',
+ scene: '',
+ status: ''
+ }
+}
+
+function createAiPromptFormState() {
+ return {
+ id: null,
+ name: '',
+ code: 'home.default',
+ scene: 'home',
+ systemPrompt: '',
+ userPromptTemplate: '',
+ status: 1,
+ memo: '',
+ updateBy: '',
+ 'updateTime$': ''
+ }
+}
+
+function getAiPromptPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+function getAiPromptStatusOptions() {
+ return STATUS_OPTIONS
+}
+
+function getAiPromptStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '鍚敤', type: 'success' }
+ : { text: '鍋滅敤', type: 'info' }
+}
+
+function buildAiPromptSearchParams(params = {}) {
+ return {
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ scene: normalizeText(params.scene),
+ status: normalizeOptionalNumber(params.status)
+ }
+}
+
+function buildAiPromptPageQueryParams(params = {}) {
+ return {
+ current: Number(params.current) > 0 ? Number(params.current) : 1,
+ pageSize: Number(params.pageSize) > 0 ? Number(params.pageSize) : 20,
+ condition: normalizeText(params.condition),
+ code: normalizeText(params.code),
+ scene: normalizeText(params.scene),
+ status: normalizeOptionalNumber(params.status)
+ }
+}
+
+function buildAiPromptDialogModel(record = {}) {
+ const base = createAiPromptFormState()
+ return {
+ ...base,
+ ...record,
+ id: normalizeOptionalNumber(record.id) ?? null,
+ status: normalizeOptionalNumber(record.status) ?? base.status,
+ code: normalizeText(record.code) || base.code,
+ scene: normalizeText(record.scene) || base.scene,
+ systemPrompt: normalizeText(record.systemPrompt),
+ userPromptTemplate: normalizeText(record.userPromptTemplate),
+ memo: normalizeText(record.memo),
+ updateBy: record.updateBy ?? '',
+ 'updateTime$': normalizeText(record['updateTime$'])
+ }
+}
+
+function buildAiPromptSavePayload(formData = {}) {
+ return {
+ ...(normalizeOptionalNumber(formData.id) !== null ? { id: normalizeOptionalNumber(formData.id) } : {}),
+ name: normalizeText(formData.name),
+ code: normalizeText(formData.code),
+ scene: normalizeText(formData.scene),
+ systemPrompt: normalizeText(formData.systemPrompt),
+ userPromptTemplate: normalizeText(formData.userPromptTemplate),
+ status: normalizeOptionalNumber(formData.status) ?? 1,
+ memo: normalizeText(formData.memo)
+ }
+}
+
+function buildAiPromptPreviewPayload(formData = {}, previewInput = '', metadata = {}) {
+ return {
+ ...buildAiPromptSavePayload(formData),
+ input: normalizeText(previewInput),
+ metadata: isPlainObject(metadata) ? metadata : {}
+ }
+}
+
+function normalizeAiPromptRow(record = {}) {
+ const statusMeta = getAiPromptStatusMeta(record.status)
+ return {
+ ...record,
+ code: normalizeText(record.code),
+ scene: normalizeText(record.scene),
+ systemPrompt: normalizeText(record.systemPrompt),
+ userPromptTemplate: normalizeText(record.userPromptTemplate),
+ memo: normalizeText(record.memo),
+ statusBool: Number(record.status) === 1,
+ statusText: statusMeta.text,
+ statusType: statusMeta.type,
+ 'createTime$': normalizeText(record['createTime$']),
+ 'updateTime$': normalizeText(record['updateTime$'])
+ }
+}
+
+function buildAiPromptPrintRows(records = []) {
+ return records.map((item) => {
+ const row = normalizeAiPromptRow(item)
+ return {
+ name: row.name || '-',
+ code: row.code || '-',
+ scene: row.scene || '-',
+ statusText: row.statusText,
+ systemPrompt: row.systemPrompt || '--',
+ userPromptTemplate: row.userPromptTemplate || '--',
+ updateTime: row['updateTime$'] || '--'
+ }
+ })
+}
+
+function getAiPromptReportColumns() {
+ return [
+ { label: '鍚嶇О', prop: 'name' },
+ { label: '缂栫爜', prop: 'code' },
+ { label: '鍦烘櫙', prop: 'scene' },
+ { label: '鐘舵��', prop: 'statusText' },
+ { label: '绯荤粺鎻愮ず璇�', prop: 'systemPrompt' },
+ { label: '鐢ㄦ埛鎻愮ず璇嶆ā鏉�', prop: 'userPromptTemplate' },
+ { label: '鏇存柊鏃堕棿', prop: 'updateTime' }
+ ]
+}
+
+function normalizeText(value) {
+ return value === undefined || value === null ? '' : String(value).trim()
+}
+
+function normalizeOptionalNumber(value) {
+ if (value === undefined || value === null || value === '') {
+ return null
+ }
+ const number = Number(value)
+ return Number.isFinite(number) ? number : null
+}
+
+function isPlainObject(value) {
+ return Object.prototype.toString.call(value) === '[object Object]'
+}
+
+export {
+ AI_PROMPT_REPORT_TITLE,
+ buildAiPromptDialogModel,
+ buildAiPromptPageQueryParams,
+ buildAiPromptPreviewPayload,
+ buildAiPromptPrintRows,
+ buildAiPromptSavePayload,
+ buildAiPromptSearchParams,
+ createAiPromptFormState,
+ createAiPromptSearchState,
+ getAiPromptPaginationKey,
+ getAiPromptReportColumns,
+ getAiPromptStatusMeta,
+ getAiPromptStatusOptions,
+ normalizeAiPromptRow
+}
diff --git a/rsf-design/src/views/system/ai-prompt/index.vue b/rsf-design/src/views/system/ai-prompt/index.vue
new file mode 100644
index 0000000..aa0ab7e
--- /dev/null
+++ b/rsf-design/src/views/system/ai-prompt/index.vue
@@ -0,0 +1,316 @@
+<template>
+ <div class="art-full-height ai-prompt-page">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <div class="mb-5 flex flex-wrap items-center justify-between gap-4">
+ <div>
+ <h3 class="text-lg font-semibold text-[var(--art-gray-900)]">Prompt 绠$悊</h3>
+ <p class="mt-1 text-sm text-[var(--art-gray-500)]">鎸夊崱鐗囩淮鎶� Prompt 妯℃澘銆佸満鏅紪鐮佸拰杩愯鏃堕瑙堛��</p>
+ </div>
+
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="openCreateDialog" v-ripple>鏂板缓 Prompt</ElButton>
+ <ElButton :loading="exportLoading" @click="handleExport" v-ripple>瀵煎嚭</ElButton>
+ <ElButton :loading="loading" @click="refreshData" v-ripple>鍒锋柊</ElButton>
+ </ElSpace>
+ </div>
+
+ <div v-loading="loading">
+ <div v-if="data.length" class="grid gap-5 md:grid-cols-2 2xl:grid-cols-3">
+ <article
+ v-for="item in data"
+ :key="item.id"
+ class="rounded-3xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] p-5 shadow-[0_12px_40px_rgba(15,23,42,0.04)]"
+ >
+ <div class="flex items-start justify-between gap-4">
+ <div class="min-w-0">
+ <div class="flex items-center gap-3">
+ <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-violet-50 text-violet-600">
+ <ArtSvgIcon icon="ri:lightbulb-flash-line" class="text-xl" />
+ </div>
+ <div class="min-w-0">
+ <h4 class="truncate text-base font-semibold text-[var(--art-gray-900)]">{{ item.name || '--' }}</h4>
+ <p class="mt-1 truncate text-sm text-[var(--art-gray-500)]">{{ item.code || '--' }}</p>
+ </div>
+ </div>
+ </div>
+
+ <ElTag :type="item.statusType" effect="light">{{ item.statusText }}</ElTag>
+ </div>
+
+ <div class="mt-4 flex flex-wrap gap-2">
+ <ElTag size="small" effect="plain">鍦烘櫙 {{ item.scene || '--' }}</ElTag>
+ </div>
+
+ <div class="mt-5 rounded-2xl bg-[var(--art-main-bg-color)]/70 p-4 ring-1 ring-inset ring-[var(--art-border-color)]">
+ <p class="text-xs text-[var(--art-gray-500)]">绯荤粺鎻愮ず璇�</p>
+ <p class="mt-2 line-clamp-4 whitespace-pre-wrap text-sm leading-6 text-[var(--art-gray-900)]">
+ {{ item.systemPrompt || '--' }}
+ </p>
+ </div>
+
+ <div class="mt-4 rounded-2xl bg-amber-50/80 px-4 py-3">
+ <p class="text-xs text-[var(--art-gray-500)]">鐢ㄦ埛鎻愮ず璇嶆ā鏉�</p>
+ <p class="mt-2 line-clamp-4 whitespace-pre-wrap text-sm leading-6 text-[var(--art-gray-900)]">
+ {{ item.userPromptTemplate || '--' }}
+ </p>
+ </div>
+
+ <div class="mt-5 flex flex-wrap items-center justify-between gap-3 border-t border-[var(--art-border-color)] pt-4">
+ <div class="flex items-center gap-2 text-xs text-[var(--art-gray-500)]">
+ <span>鏇存柊鏃堕棿</span>
+ <span>{{ item['updateTime$'] || '--' }}</span>
+ </div>
+
+ <ElSpace wrap>
+ <ElButton text @click="openDetailDialog(item)">璇︽儏</ElButton>
+ <ElButton v-auth="'edit'" text @click="openEditDialog(item)">缂栬緫</ElButton>
+ <ElButton v-auth="'delete'" text type="danger" @click="handleDelete(item)">鍒犻櫎</ElButton>
+ </ElSpace>
+ </div>
+ </article>
+ </div>
+
+ <ElEmpty v-else description="鏆傛棤 Prompt 鏁版嵁" :image-size="110" />
+ </div>
+
+ <div class="mt-6 flex justify-end">
+ <ElPagination
+ background
+ layout="total, sizes, prev, pager, next, jumper"
+ :current-page="pagination.current"
+ :page-size="pagination.size"
+ :total="pagination.total"
+ :page-sizes="[20, 50, 100]"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+
+ <AiPromptDialog
+ v-model:visible="dialogVisible"
+ :mode="dialogMode"
+ :ai-prompt-data="currentAiPromptData"
+ @submit="handleDialogSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchAiPromptPage,
+ fetchDeleteAiPrompt,
+ fetchExportAiPromptReport,
+ fetchGetAiPromptDetail,
+ fetchSaveAiPrompt,
+ fetchUpdateAiPrompt
+ } from '@/api/ai-config'
+ import AiPromptDialog from './modules/ai-prompt-dialog.vue'
+ import {
+ buildAiPromptDialogModel,
+ buildAiPromptPageQueryParams,
+ buildAiPromptSavePayload,
+ buildAiPromptSearchParams,
+ createAiPromptSearchState,
+ getAiPromptPaginationKey,
+ normalizeAiPromptRow
+ } from './aiPromptPage.helpers'
+
+ defineOptions({ name: 'AiPrompt' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createAiPromptSearchState())
+ const dialogVisible = ref(false)
+ const dialogMode = ref('create')
+ const currentAiPromptData = ref(buildAiPromptDialogModel())
+ const exportLoading = ref(false)
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� Prompt 鍚嶇О'
+ }
+ },
+ {
+ label: '缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� Prompt 缂栫爜'
+ }
+ },
+ {
+ label: '鍦烘櫙',
+ key: 'scene',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ満鏅爣璇�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鍚敤', value: 1 },
+ { label: '鍋滅敤', value: 0 }
+ ]
+ }
+ }
+ ])
+
+ const {
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchAiPromptPage,
+ apiParams: buildAiPromptPageQueryParams(searchForm.value),
+ paginationKey: getAiPromptPaginationKey()
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeAiPromptRow(item))
+ }
+ }
+ })
+
+ async function openEditDialog(record) {
+ try {
+ currentAiPromptData.value = buildAiPromptDialogModel(await fetchGetAiPromptDetail(record.id))
+ dialogMode.value = 'edit'
+ dialogVisible.value = true
+ } catch {
+ return
+ }
+ }
+
+ async function openDetailDialog(record) {
+ try {
+ currentAiPromptData.value = buildAiPromptDialogModel(await fetchGetAiPromptDetail(record.id))
+ dialogMode.value = 'show'
+ dialogVisible.value = true
+ } catch {
+ return
+ }
+ }
+
+ function openCreateDialog() {
+ currentAiPromptData.value = buildAiPromptDialogModel()
+ dialogMode.value = 'create'
+ dialogVisible.value = true
+ }
+
+ async function handleDialogSubmit(payload) {
+ try {
+ if (dialogMode.value === 'edit') {
+ await fetchUpdateAiPrompt(buildAiPromptSavePayload(payload))
+ ElMessage.success('淇敼鎴愬姛')
+ dialogVisible.value = false
+ await refreshUpdate()
+ return
+ }
+ await fetchSaveAiPrompt(buildAiPromptSavePayload(payload))
+ ElMessage.success('鏂板鎴愬姛')
+ dialogVisible.value = false
+ await refreshCreate()
+ } catch {
+ return
+ }
+ }
+
+ async function handleDelete(record) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄� Prompt銆�${record?.name || record?.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteAiPrompt(record.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshRemove()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ async function handleExport() {
+ exportLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(
+ fetchExportAiPromptReport(buildAiPromptSearchParams(searchForm.value), {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ null,
+ {
+ timeoutMessage: '瀵煎嚭璇锋眰瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ if (!response) return
+ if (!response.ok) {
+ throw new Error(`瀵煎嚭澶辫触 (${response.status})`)
+ }
+ const blob = await response.blob()
+ const url = window.URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = 'ai-prompt.xlsx'
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ window.URL.revokeObjectURL(url)
+ ElMessage.success('瀵煎嚭鎴愬姛')
+ } catch (error) {
+ ElMessage.error(error?.message || '瀵煎嚭澶辫触')
+ } finally {
+ exportLoading.value = false
+ }
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildAiPromptSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createAiPromptSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/ai-prompt/modules/ai-prompt-dialog.vue b/rsf-design/src/views/system/ai-prompt/modules/ai-prompt-dialog.vue
new file mode 100644
index 0000000..67211b6
--- /dev/null
+++ b/rsf-design/src/views/system/ai-prompt/modules/ai-prompt-dialog.vue
@@ -0,0 +1,303 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="980px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <div class="mt-2 rounded-2xl border border-[var(--art-border-color)] px-5 py-4">
+ <div class="mb-3 flex items-center justify-between gap-4">
+ <div>
+ <h4 class="text-base font-semibold text-[var(--art-gray-900)]">娓叉煋棰勮</h4>
+ <p class="mt-1 text-sm text-[var(--art-gray-500)]">杈撳叆绀轰緥鍐呭鍜� metadata锛岀洿鎺ラ瑙堟渶缁堟覆鏌撶粨鏋溿��</p>
+ </div>
+ <ElButton v-if="!isReadonly" :loading="previewLoading" @click="handlePreview">娓叉煋棰勮</ElButton>
+ </div>
+
+ <div class="grid gap-4 lg:grid-cols-2">
+ <div class="space-y-4">
+ <ElInput
+ v-model="previewInput"
+ type="textarea"
+ :rows="4"
+ :disabled="isReadonly"
+ placeholder="璇疯緭鍏ョず渚嬭緭鍏ュ唴瀹�"
+ />
+ <ElInput
+ v-model="metadataText"
+ type="textarea"
+ :rows="4"
+ :disabled="isReadonly"
+ placeholder='璇疯緭鍏� JSON metadata锛屼緥濡� {"path":"/system/aiPrompt"}'
+ />
+ </div>
+
+ <div class="space-y-4">
+ <ElAlert
+ v-if="previewErrorMessage"
+ type="warning"
+ :closable="false"
+ :title="previewErrorMessage"
+ />
+ <ElAlert
+ v-else-if="previewResult"
+ type="success"
+ :closable="false"
+ :title="`宸茶В鏋愬彉閲忥細${resolvedVariablesText}`"
+ />
+ <ElInput
+ :model-value="previewResult?.renderedSystemPrompt || ''"
+ type="textarea"
+ :rows="5"
+ readonly
+ placeholder="绯荤粺鎻愮ず璇嶆覆鏌撶粨鏋�"
+ />
+ <ElInput
+ :model-value="previewResult?.renderedUserPrompt || ''"
+ type="textarea"
+ :rows="5"
+ readonly
+ placeholder="鐢ㄦ埛鎻愮ず璇嶆覆鏌撶粨鏋�"
+ />
+ </div>
+ </div>
+ </div>
+
+ <div v-if="showRuntimeSection" class="mt-5 rounded-2xl border border-[var(--art-border-color)] px-5 py-4">
+ <h4 class="text-base font-semibold text-[var(--art-gray-900)]">杩愯鏃剁姸鎬�</h4>
+ <ElDescriptions class="mt-4" :column="2" border>
+ <ElDescriptionsItem label="鏈�杩戞洿鏂颁汉">{{ form.updateBy || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�杩戞洿鏂版椂闂�">{{ form['updateTime$'] || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">{{ isReadonly ? '鍏抽棴' : '鍙栨秷' }}</ElButton>
+ <ElButton v-if="!isReadonly" type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { fetchRenderAiPromptPreview } from '@/api/ai-config'
+ import {
+ buildAiPromptDialogModel,
+ buildAiPromptPreviewPayload,
+ buildAiPromptSavePayload,
+ createAiPromptFormState,
+ getAiPromptStatusOptions
+ } from '../aiPromptPage.helpers'
+
+ const DEFAULT_PREVIEW_INPUT = '璇锋牴鎹綋鍓嶈緭鍏ョ粰鍑烘憳瑕�'
+ const DEFAULT_METADATA_TEXT = '{"path":"/system/aiPrompt"}'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ mode: { type: String, default: 'create' },
+ aiPromptData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['submit', 'update:visible'])
+ const formRef = ref()
+ const form = reactive(createAiPromptFormState())
+ const previewInput = ref(DEFAULT_PREVIEW_INPUT)
+ const metadataText = ref(DEFAULT_METADATA_TEXT)
+ const previewLoading = ref(false)
+ const previewResult = ref(null)
+ const previewErrorMessage = ref('')
+
+ const isReadonly = computed(() => props.mode === 'show')
+ const showRuntimeSection = computed(() => Boolean(form.id) || props.mode !== 'create')
+ const dialogTitle = computed(() => {
+ if (props.mode === 'edit') return '缂栬緫 Prompt'
+ if (props.mode === 'show') return 'Prompt 璇︽儏'
+ return '鏂板缓 Prompt'
+ })
+
+ const resolvedVariablesText = computed(() => {
+ if (!Array.isArray(previewResult.value?.resolvedVariables) || !previewResult.value.resolvedVariables.length) {
+ return '鏃�'
+ }
+ return previewResult.value.resolvedVariables.join('銆�')
+ })
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏� Prompt 鍚嶇О', trigger: 'blur' }],
+ code: [{ required: true, message: '璇疯緭鍏� Prompt 缂栫爜', trigger: 'blur' }],
+ scene: [{ required: true, message: '璇疯緭鍏ュ満鏅爣璇�', trigger: 'blur' }],
+ systemPrompt: [{ required: true, message: '璇疯緭鍏ョ郴缁熸彁绀鸿瘝', trigger: 'blur' }],
+ userPromptTemplate: [{ required: true, message: '璇疯緭鍏ョ敤鎴锋彁绀鸿瘝妯℃澘', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: 'Prompt 鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� Prompt 鍚嶇О',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: 'Prompt 缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� Prompt 缂栫爜',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '鍦烘櫙鏍囪瘑',
+ key: 'scene',
+ type: 'input',
+ span: 24,
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ満鏅爣璇�',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '绯荤粺鎻愮ず璇�',
+ key: 'systemPrompt',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 6,
+ placeholder: '璇疯緭鍏ョ郴缁熸彁绀鸿瘝',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '鐢ㄦ埛鎻愮ず璇嶆ā鏉�',
+ key: 'userPromptTemplate',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 5,
+ placeholder: '璇疯緭鍏ョ敤鎴锋彁绀鸿瘝妯℃澘',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: getAiPromptStatusOptions(),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ disabled: isReadonly.value
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ disabled: isReadonly.value
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createAiPromptFormState())
+ previewInput.value = DEFAULT_PREVIEW_INPUT
+ metadataText.value = DEFAULT_METADATA_TEXT
+ previewResult.value = null
+ previewErrorMessage.value = ''
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildAiPromptDialogModel(props.aiPromptData))
+ previewResult.value = null
+ previewErrorMessage.value = ''
+ }
+
+ function parseMetadataText() {
+ const text = String(metadataText.value || '').trim()
+ if (!text) return {}
+ return JSON.parse(text)
+ }
+
+ async function handlePreview() {
+ previewLoading.value = true
+ previewErrorMessage.value = ''
+ try {
+ previewResult.value = await fetchRenderAiPromptPreview(
+ buildAiPromptPreviewPayload(form, previewInput.value, parseMetadataText())
+ )
+ } catch (error) {
+ previewResult.value = null
+ previewErrorMessage.value = error?.message || '娓叉煋棰勮澶辫触'
+ } finally {
+ previewLoading.value = false
+ }
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', buildAiPromptSavePayload(form))
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.aiPromptData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/common/usePrintExportPage.js b/rsf-design/src/views/system/common/usePrintExportPage.js
index c202543..49fdd41 100644
--- a/rsf-design/src/views/system/common/usePrintExportPage.js
+++ b/rsf-design/src/views/system/common/usePrintExportPage.js
@@ -1,12 +1,14 @@
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
+import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
export function usePrintExportPage({
downloadFileName,
requestExport,
resolvePrintRecords,
buildPreviewRows,
- buildPreviewMeta
+ buildPreviewMeta,
+ timeoutMs
}) {
const previewVisible = ref(false)
const previewRows = ref([])
@@ -23,7 +25,13 @@
const handleExport = async (payload) => {
try {
- const response = await requestExport(payload)
+ const response = await guardRequestWithMessage(requestExport(payload), null, {
+ timeoutMs,
+ timeoutMessage: '瀵煎嚭璇锋眰瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ if (!response) {
+ return
+ }
if (!response.ok) {
throw new Error(`瀵煎嚭澶辫触 (${response.status})`)
}
@@ -52,10 +60,16 @@
previewMeta.value = {}
try {
- const records = await resolvePrintRecords(payload)
+ const records = await guardRequestWithMessage(resolvePrintRecords(payload), null, {
+ timeoutMs,
+ timeoutMessage: '鎵撳嵃鏁版嵁鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
if (activePrintToken.value !== token) {
return
}
+ if (!records) {
+ return
+ }
const rows = buildPreviewRows(records)
previewRows.value = rows
diff --git a/rsf-design/src/views/system/config/configPage.helpers.js b/rsf-design/src/views/system/config/configPage.helpers.js
new file mode 100644
index 0000000..4564433
--- /dev/null
+++ b/rsf-design/src/views/system/config/configPage.helpers.js
@@ -0,0 +1,119 @@
+const CONFIG_TYPE_OPTIONS = [
+ { label: 'boolean', value: 1 },
+ { label: 'number', value: 2 },
+ { label: 'string', value: 3 },
+ { label: 'json', value: 4 },
+ { label: 'date', value: 5 }
+]
+
+export function createConfigSearchState() {
+ return {
+ condition: '',
+ flag: '',
+ type: '',
+ status: ''
+ }
+}
+
+export function createConfigFormState() {
+ return {
+ id: null,
+ uuid: '',
+ name: '',
+ flag: '',
+ type: 3,
+ val: '',
+ content: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getConfigPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getConfigTypeOptions() {
+ return CONFIG_TYPE_OPTIONS
+}
+
+export function getConfigTypeMeta(type) {
+ const normalizedType = Number(type)
+ return CONFIG_TYPE_OPTIONS.find((item) => item.value === normalizedType) || { label: '-', value: normalizedType || '' }
+}
+
+export function getConfigStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '姝e父', type: 'success', bool: true }
+ : { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export function buildConfigSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ flag: String(params.flag || '').trim(),
+ ...(params.type !== '' && params.type !== null && params.type !== undefined ? { type: Number(params.type) } : {}),
+ ...(params.status !== '' && params.status !== null && params.status !== undefined
+ ? { status: Number(params.status) }
+ : {})
+ }
+}
+
+export function buildConfigPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildConfigSearchParams(params)
+ }
+}
+
+export function buildConfigDialogModel(record = {}) {
+ return {
+ ...createConfigFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ uuid: record.uuid || '',
+ name: record.name || '',
+ flag: record.flag || '',
+ type: record.type !== undefined && record.type !== null ? Number(record.type) : 3,
+ val: record.val || '',
+ content: record.content || '',
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildConfigSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ uuid: String(formData.uuid || '').trim(),
+ name: String(formData.name || '').trim(),
+ flag: String(formData.flag || '').trim(),
+ type: Number(formData.type || 3),
+ val: String(formData.val || '').trim(),
+ content: String(formData.content || '').trim(),
+ status: Number(formData.status ?? 1),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function normalizeConfigListRow(record = {}) {
+ const statusMeta = getConfigStatusMeta(record.status)
+ return {
+ ...record,
+ uuid: record.uuid || '',
+ name: record.name || '',
+ flag: record.flag || '',
+ val: record.val || '',
+ content: record.content || '',
+ memo: record.memo || '',
+ typeText: record['type$'] || getConfigTypeMeta(record.type).label,
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
diff --git a/rsf-design/src/views/system/config/configTable.columns.js b/rsf-design/src/views/system/config/configTable.columns.js
new file mode 100644
index 0000000..f61a9e4
--- /dev/null
+++ b/rsf-design/src/views/system/config/configTable.columns.js
@@ -0,0 +1,80 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createConfigTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'uuid',
+ label: '缂栧彿',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'name',
+ label: '鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'flag',
+ label: '鏍囪瘑',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'typeText',
+ label: '绫诲瀷',
+ width: 110,
+ formatter: (row) => row.typeText || '-'
+ },
+ {
+ prop: 'val',
+ label: '鍊�',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.val || '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/config/index.vue b/rsf-design/src/views/system/config/index.vue
new file mode 100644
index 0000000..a879fe6
--- /dev/null
+++ b/rsf-design/src/views/system/config/index.vue
@@ -0,0 +1,226 @@
+<template>
+ <div class="config-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板閰嶇疆</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <ConfigDialog
+ v-model:visible="dialogVisible"
+ :config-data="currentConfigData"
+ @submit="handleDialogSubmit"
+ />
+
+ <ConfigDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import {
+ fetchConfigPage,
+ fetchDeleteConfig,
+ fetchGetConfigDetail,
+ fetchSaveConfig,
+ fetchUpdateConfig
+ } from '@/api/system-manage'
+ import ConfigDialog from './modules/config-dialog.vue'
+ import ConfigDetailDrawer from './modules/config-detail-drawer.vue'
+ import { createConfigTableColumns } from './configTable.columns'
+ import {
+ buildConfigDialogModel,
+ buildConfigPageQueryParams,
+ buildConfigSavePayload,
+ buildConfigSearchParams,
+ createConfigSearchState,
+ getConfigPaginationKey,
+ getConfigTypeOptions,
+ normalizeConfigListRow
+ } from './configPage.helpers'
+
+ defineOptions({ name: 'Config' })
+
+ const { hasAuth } = useAuth()
+ const searchForm = ref(createConfigSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ厤缃悕绉�'
+ }
+ },
+ {
+ label: '鏍囪瘑',
+ key: 'flag',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ厤缃爣璇�'
+ }
+ },
+ {
+ label: '绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getConfigTypeOptions()
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeConfigListRow(await fetchGetConfigDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇閰嶇疆璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentConfigData.value = buildConfigDialogModel(await fetchGetConfigDetail(row.id))
+ dialogVisible.value = true
+ dialogType.value = 'edit'
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇閰嶇疆璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchConfigPage,
+ apiParams: buildConfigPageQueryParams(searchForm.value),
+ paginationKey: getConfigPaginationKey(),
+ columnsFactory: () =>
+ createConfigTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeConfigListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentConfigData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildConfigDialogModel(),
+ buildEditModel: (record) => buildConfigDialogModel(record),
+ buildSavePayload: (formData) => buildConfigSavePayload(formData),
+ saveRequest: fetchSaveConfig,
+ updateRequest: fetchUpdateConfig,
+ deleteRequest: fetchDeleteConfig,
+ entityName: '閰嶇疆',
+ resolveRecordLabel: (record) => record?.name || record?.flag || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ function handleSearch(params) {
+ replaceSearchParams(buildConfigSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createConfigSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/config/modules/config-detail-drawer.vue b/rsf-design/src/views/system/config/modules/config-detail-drawer.vue
new file mode 100644
index 0000000..0e7105d
--- /dev/null
+++ b/rsf-design/src/views/system/config/modules/config-detail-drawer.vue
@@ -0,0 +1,42 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="閰嶇疆璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="缂栧彿">{{ displayData.uuid || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚嶇О">{{ displayData.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏍囪瘑">{{ displayData.flag || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绫诲瀷">{{ displayData.typeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍊�">{{ displayData.val || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏂囨湰">{{ displayData.content || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeConfigListRow } from '../configPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeConfigListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/config/modules/config-dialog.vue b/rsf-design/src/views/system/config/modules/config-dialog.vue
new file mode 100644
index 0000000..bb56df3
--- /dev/null
+++ b/rsf-design/src/views/system/config/modules/config-dialog.vue
@@ -0,0 +1,181 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildConfigDialogModel, createConfigFormState, getConfigTypeOptions } from '../configPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ configData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createConfigFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫閰嶇疆' : '鏂板閰嶇疆'))
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ラ厤缃悕绉�', trigger: 'blur' }],
+ flag: [{ required: true, message: '璇疯緭鍏ラ厤缃爣璇�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '缂栧彿',
+ key: 'uuid',
+ type: 'input',
+ props: {
+ disabled: true,
+ placeholder: '鏂板鍚庤嚜鍔ㄧ敓鎴�'
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ厤缃悕绉�'
+ }
+ },
+ {
+ label: '鏍囪瘑',
+ key: 'flag',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ厤缃爣璇�'
+ }
+ },
+ {
+ label: '绫诲瀷',
+ key: 'type',
+ type: 'select',
+ props: {
+ options: getConfigTypeOptions(),
+ placeholder: '璇烽�夋嫨绫诲瀷'
+ }
+ },
+ {
+ label: '鍊�',
+ key: 'val',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ厤缃��'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '鏂囨湰',
+ key: 'content',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ラ厤缃枃鏈�'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createConfigFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildConfigDialogModel(props.configData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.configData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/dept/deptPage.helpers.js b/rsf-design/src/views/system/dept/deptPage.helpers.js
new file mode 100644
index 0000000..47fad23
--- /dev/null
+++ b/rsf-design/src/views/system/dept/deptPage.helpers.js
@@ -0,0 +1,137 @@
+function createDeptSearchState() {
+ return {
+ condition: ''
+ }
+}
+
+function createDeptFormState() {
+ return {
+ id: null,
+ parentId: 0,
+ name: '',
+ fullName: '',
+ leader: '',
+ sort: 0,
+ status: 1,
+ memo: ''
+ }
+}
+
+function normalizeDeptTreeRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records.map((item) => {
+ const id = normalizeDeptId(item?.id)
+ const children = normalizeDeptTreeRows(item?.children || [])
+ return {
+ ...item,
+ id,
+ parentId: normalizeDeptId(item?.parentId),
+ name: item?.name || '',
+ fullName: item?.fullName || '-',
+ leader: item?.leader || '-',
+ sort: normalizeDeptNumber(item?.sort, 0),
+ status: normalizeDeptNumber(item?.status, 1),
+ statusBool: normalizeDeptNumber(item?.status, 1) === 1,
+ statusText: normalizeDeptNumber(item?.status, 1) === 1 ? '姝e父' : '绂佺敤',
+ statusType: normalizeDeptNumber(item?.status, 1) === 1 ? 'success' : 'danger',
+ updateTimeText: item?.updateTime$ || item?.updateTime || '-',
+ createTimeText: item?.createTime$ || item?.createTime || '-',
+ memo: item?.memo || '-',
+ children
+ }
+ })
+}
+
+function buildDeptTreeOptions(records = [], currentId = null) {
+ const normalizedCurrentId = normalizeDeptId(currentId)
+
+ return normalizeDeptTreeRows(records)
+ .filter((item) => item.id !== normalizedCurrentId)
+ .map((item) => mapDeptTreeOption(item, normalizedCurrentId))
+ .filter(Boolean)
+}
+
+function mapDeptTreeOption(item, currentId) {
+ if (!item || item.id === currentId) {
+ return null
+ }
+
+ const children = Array.isArray(item.children)
+ ? item.children.map((child) => mapDeptTreeOption(child, currentId)).filter(Boolean)
+ : []
+
+ return {
+ value: item.id,
+ label: item.name || '-',
+ children
+ }
+}
+
+function buildDeptSearchParams(params = {}) {
+ return {
+ ...(params.condition !== undefined ? { condition: String(params.condition || '').trim() } : {})
+ }
+}
+
+function buildDeptDialogModel(record = {}) {
+ return {
+ id: normalizeDeptNullableId(record?.id),
+ parentId: normalizeDeptId(record?.parentId),
+ name: record?.name || '',
+ fullName: record?.fullName || '',
+ leader: record?.leader || '',
+ sort: normalizeDeptNumber(record?.sort, 0),
+ status: normalizeDeptNumber(record?.status, 1),
+ memo: record?.memo || ''
+ }
+}
+
+function buildDeptSavePayload(form = {}) {
+ return {
+ ...(form.id ? { id: normalizeDeptId(form.id) } : {}),
+ parentId: normalizeDeptId(form.parentId),
+ name: String(form.name || '').trim(),
+ fullName: String(form.fullName || '').trim(),
+ leader: String(form.leader || '').trim(),
+ sort: normalizeDeptNumber(form.sort, 0),
+ status: normalizeDeptNumber(form.status, 1),
+ memo: String(form.memo || '').trim()
+ }
+}
+
+function normalizeDeptId(value) {
+ if (value === null || value === undefined || value === '') {
+ return 0
+ }
+ const normalized = Number(value)
+ return Number.isFinite(normalized) ? normalized : 0
+}
+
+function normalizeDeptNullableId(value) {
+ if (value === null || value === undefined || value === '') {
+ return null
+ }
+ const normalized = Number(value)
+ return Number.isFinite(normalized) ? normalized : null
+}
+
+function normalizeDeptNumber(value, fallback = 0) {
+ if (value === null || value === undefined || value === '') {
+ return fallback
+ }
+ const normalized = Number(value)
+ return Number.isFinite(normalized) ? normalized : fallback
+}
+
+export {
+ buildDeptDialogModel,
+ buildDeptSavePayload,
+ buildDeptSearchParams,
+ buildDeptTreeOptions,
+ createDeptFormState,
+ createDeptSearchState,
+ normalizeDeptTreeRows
+}
diff --git a/rsf-design/src/views/system/dept/deptTable.columns.js b/rsf-design/src/views/system/dept/deptTable.columns.js
new file mode 100644
index 0000000..001815e
--- /dev/null
+++ b/rsf-design/src/views/system/dept/deptTable.columns.js
@@ -0,0 +1,69 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createDeptTableColumns({ handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'name',
+ label: '閮ㄩ棬鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'fullName',
+ label: '閮ㄩ棬鍏ㄧО',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.fullName || '-'
+ },
+ {
+ prop: 'leader',
+ label: '璐熻矗浜�',
+ minWidth: 140,
+ formatter: (row) => row.leader || '-'
+ },
+ {
+ prop: 'sort',
+ label: '鎺掑簭',
+ width: 90
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) =>
+ h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 140,
+ align: 'right',
+ formatter: (row) =>
+ h('div', { class: 'flex justify-end' }, [
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ ])
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/dept/index.vue b/rsf-design/src/views/system/dept/index.vue
new file mode 100644
index 0000000..2dbb5b6
--- /dev/null
+++ b/rsf-design/src/views/system/dept/index.vue
@@ -0,0 +1,211 @@
+<template>
+ <div class="dept-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :show-expand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader
+ :loading="loading"
+ :show-zebra="false"
+ v-model:columns="columnChecks"
+ @refresh="handleRefresh"
+ >
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="handleAdd" v-ripple>鏂板閮ㄩ棬</ElButton>
+ <ElButton @click="toggleExpand" v-ripple>
+ {{ isExpanded ? '鏀惰捣' : '灞曞紑' }}
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ ref="tableRef"
+ rowKey="id"
+ :loading="loading"
+ :columns="columns"
+ :data="tableData"
+ :stripe="false"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ :default-expand-all="false"
+ />
+
+ <DeptDialog
+ v-model:visible="dialogVisible"
+ :dept-data="currentDeptData"
+ :dept-tree-options="deptTreeOptions"
+ @submit="handleDialogSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import {
+ fetchDeleteDept,
+ fetchGetDeptDetail,
+ fetchGetDeptTree,
+ fetchSaveDept,
+ fetchUpdateDept
+ } from '@/api/system-manage'
+ import DeptDialog from './modules/dept-dialog.vue'
+ import { createDeptTableColumns } from './deptTable.columns'
+ import {
+ buildDeptDialogModel,
+ buildDeptSavePayload,
+ buildDeptSearchParams,
+ buildDeptTreeOptions,
+ createDeptSearchState,
+ normalizeDeptTreeRows
+ } from './deptPage.helpers'
+
+ defineOptions({ name: 'Dept' })
+
+ const loading = ref(false)
+ const isExpanded = ref(false)
+ const tableRef = ref()
+ const dialogVisible = ref(false)
+ const currentDeptData = ref(buildDeptDialogModel())
+ const deptTreeOptions = ref([])
+ const tableData = ref([])
+
+ const initialSearchState = createDeptSearchState()
+ const searchForm = reactive({ ...initialSearchState })
+
+ const searchItems = computed(() => [
+ {
+ label: '閮ㄩ棬鍚嶇О',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ラ儴闂ㄥ悕绉�'
+ }
+ }
+ ])
+
+ const { columnChecks, columns } = useTableColumns(() =>
+ createDeptTableColumns({
+ handleEdit: handleEdit,
+ handleDelete: handleDelete
+ })
+ )
+
+ onMounted(() => {
+ loadDeptTree()
+ })
+
+ async function loadDeptTree() {
+ loading.value = true
+ try {
+ const tree = await guardRequestWithMessage(
+ fetchGetDeptTree(buildDeptSearchParams(searchForm)),
+ [],
+ {
+ timeoutMessage: '閮ㄩ棬鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ }
+ )
+ const normalizedRows = normalizeDeptTreeRows(tree || [])
+ tableData.value = normalizedRows
+ deptTreeOptions.value = buildDeptTreeOptions(normalizedRows)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ function handleSearch() {
+ loadDeptTree()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm, { ...initialSearchState })
+ loadDeptTree()
+ }
+
+ function handleRefresh() {
+ loadDeptTree()
+ }
+
+ function handleAdd() {
+ currentDeptData.value = buildDeptDialogModel()
+ deptTreeOptions.value = buildDeptTreeOptions(tableData.value)
+ dialogVisible.value = true
+ }
+
+ async function handleEdit(row) {
+ try {
+ const detail = await fetchGetDeptDetail(row.id)
+ currentDeptData.value = buildDeptDialogModel(detail || row)
+ deptTreeOptions.value = buildDeptTreeOptions(tableData.value, row.id)
+ dialogVisible.value = true
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇閮ㄩ棬璇︽儏澶辫触')
+ }
+ }
+
+ async function handleDelete(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄ら儴闂ㄣ��${row.name || row.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteDept(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await loadDeptTree()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ async function handleDialogSubmit(formData) {
+ const payload = buildDeptSavePayload(formData)
+ if (payload.id && payload.id === payload.parentId) {
+ ElMessage.error('涓婄骇閮ㄩ棬涓嶈兘閫夋嫨褰撳墠閮ㄩ棬')
+ return
+ }
+
+ try {
+ if (payload.id) {
+ await fetchUpdateDept(payload)
+ ElMessage.success('淇敼鎴愬姛')
+ } else {
+ await fetchSaveDept(payload)
+ ElMessage.success('鏂板鎴愬姛')
+ }
+ dialogVisible.value = false
+ currentDeptData.value = buildDeptDialogModel()
+ await loadDeptTree()
+ } catch (error) {
+ ElMessage.error(error?.message || '鎻愪氦澶辫触')
+ }
+ }
+
+ function toggleExpand() {
+ isExpanded.value = !isExpanded.value
+ nextTick(() => {
+ if (tableRef.value?.elTableRef && tableData.value) {
+ const processRows = (rows) => {
+ rows.forEach((row) => {
+ if (row.children?.length) {
+ tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
+ processRows(row.children)
+ }
+ })
+ }
+ processRows(tableData.value)
+ }
+ })
+ }
+</script>
diff --git a/rsf-design/src/views/system/dept/modules/dept-dialog.vue b/rsf-design/src/views/system/dept/modules/dept-dialog.vue
new file mode 100644
index 0000000..650083b
--- /dev/null
+++ b/rsf-design/src/views/system/dept/modules/dept-dialog.vue
@@ -0,0 +1,183 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="760px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildDeptDialogModel, createDeptFormState } from '../deptPage.helpers'
+
+ const props = defineProps({
+ visible: { required: false, default: false },
+ deptData: { required: false, default: () => ({}) },
+ deptTreeOptions: { required: false, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createDeptFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫閮ㄩ棬' : '鏂板閮ㄩ棬'))
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ラ儴闂ㄥ悕绉�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '涓婄骇閮ㄩ棬',
+ key: 'parentId',
+ type: 'treeselect',
+ span: 24,
+ props: {
+ data: props.deptTreeOptions,
+ props: {
+ label: 'label',
+ value: 'value',
+ children: 'children'
+ },
+ placeholder: '璇烽�夋嫨涓婄骇閮ㄩ棬',
+ clearable: false,
+ checkStrictly: true,
+ defaultExpandAll: true
+ }
+ },
+ {
+ label: '閮ㄩ棬鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ儴闂ㄥ悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '閮ㄩ棬鍏ㄧО',
+ key: 'fullName',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ儴闂ㄥ叏绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '璐熻矗浜�',
+ key: 'leader',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ヨ礋璐d汉',
+ clearable: true
+ }
+ },
+ {
+ label: '鎺掑簭',
+ key: 'sort',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '绂佺敤', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�',
+ clearable: true
+ }
+ }
+ ])
+
+ const resetForm = () => {
+ Object.assign(form, createDeptFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ const loadFormData = () => {
+ Object.assign(form, buildDeptDialogModel(props.deptData))
+ }
+
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+
+ const handleClosed = () => {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate?.()
+ })
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.deptData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js b/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js
new file mode 100644
index 0000000..a141bdc
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js
@@ -0,0 +1,92 @@
+export function createDictTypeSearchState() {
+ return {
+ condition: '',
+ code: '',
+ name: '',
+ status: ''
+ }
+}
+
+export function createDictTypeFormState() {
+ return {
+ id: null,
+ code: '',
+ name: '',
+ description: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getDictTypePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getDictTypeStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '姝e父', type: 'success', bool: true }
+ : { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export function buildDictTypeSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ code: String(params.code || '').trim(),
+ name: String(params.name || '').trim(),
+ ...(params.status !== '' && params.status !== null && params.status !== undefined
+ ? { status: Number(params.status) }
+ : {})
+ }
+}
+
+export function buildDictTypePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildDictTypeSearchParams(params)
+ }
+}
+
+export function buildDictTypeDialogModel(record = {}) {
+ return {
+ ...createDictTypeFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ code: record.code || '',
+ name: record.name || '',
+ description: record.description || '',
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildDictTypeSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ code: String(formData.code || '').trim(),
+ name: String(formData.name || '').trim(),
+ description: String(formData.description || '').trim(),
+ status: Number(formData.status ?? 1),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function normalizeDictTypeListRow(record = {}) {
+ const statusMeta = getDictTypeStatusMeta(record.status)
+ return {
+ ...record,
+ code: record.code || '',
+ name: record.name || '',
+ description: record.description || '',
+ memo: record.memo || '',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateByLabel: record['updateBy$'] || record.updateByLabel || '',
+ createByLabel: record['createBy$'] || record.createByLabel || '',
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
diff --git a/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js b/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js
new file mode 100644
index 0000000..5426adc
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js
@@ -0,0 +1,74 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createDictTypeTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'code',
+ label: '缂栫爜',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'name',
+ label: '鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'description',
+ label: '鎻忚堪',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.description || '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateByLabel',
+ label: '鏇存柊浜�',
+ width: 120,
+ formatter: (row) => row.updateByLabel || '-'
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/dict-type/index.vue b/rsf-design/src/views/system/dict-type/index.vue
new file mode 100644
index 0000000..b346aad
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/index.vue
@@ -0,0 +1,225 @@
+<template>
+ <div class="dict-type-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鏁版嵁瀛楀吀</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <DictTypeDialog
+ v-model:visible="dialogVisible"
+ :dict-type-data="currentDictTypeData"
+ @submit="handleDialogSubmit"
+ />
+
+ <DictTypeDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import {
+ fetchDeleteDictType,
+ fetchDictTypePage,
+ fetchGetDictTypeDetail,
+ fetchSaveDictType,
+ fetchUpdateDictType
+ } from '@/api/system-manage'
+ import DictTypeDialog from './modules/dict-type-dialog.vue'
+ import DictTypeDetailDrawer from './modules/dict-type-detail-drawer.vue'
+ import { createDictTypeTableColumns } from './dictTypeTable.columns'
+ import {
+ buildDictTypeDialogModel,
+ buildDictTypePageQueryParams,
+ buildDictTypeSavePayload,
+ buildDictTypeSearchParams,
+ createDictTypeSearchState,
+ getDictTypePaginationKey,
+ normalizeDictTypeListRow
+ } from './dictTypePage.helpers'
+
+ defineOptions({ name: 'DictType' })
+
+ const { hasAuth } = useAuth()
+ const searchForm = ref(createDictTypeSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鐮佹垨鍚嶇О'
+ }
+ },
+ {
+ label: '缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧鍏哥紪鐮�'
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧鍏稿悕绉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeDictTypeListRow(await fetchGetDictTypeDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鏁版嵁瀛楀吀璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentDictTypeData.value = buildDictTypeDialogModel(await fetchGetDictTypeDetail(row.id))
+ dialogVisible.value = true
+ dialogType.value = 'edit'
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇鏁版嵁瀛楀吀璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchDictTypePage,
+ apiParams: buildDictTypePageQueryParams(searchForm.value),
+ paginationKey: getDictTypePaginationKey(),
+ columnsFactory: () =>
+ createDictTypeTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeDictTypeListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentDictTypeData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildDictTypeDialogModel(),
+ buildEditModel: (record) => buildDictTypeDialogModel(record),
+ buildSavePayload: (formData) => buildDictTypeSavePayload(formData),
+ saveRequest: fetchSaveDictType,
+ updateRequest: fetchUpdateDictType,
+ deleteRequest: fetchDeleteDictType,
+ entityName: '鏁版嵁瀛楀吀',
+ resolveRecordLabel: (record) => record?.name || record?.code || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ function handleSearch(params) {
+ replaceSearchParams(buildDictTypeSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createDictTypeSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/dict-type/modules/dict-type-detail-drawer.vue b/rsf-design/src/views/system/dict-type/modules/dict-type-detail-drawer.vue
new file mode 100644
index 0000000..9863bb3
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/modules/dict-type-detail-drawer.vue
@@ -0,0 +1,41 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鏁版嵁瀛楀吀璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="缂栫爜">{{ displayData.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚嶇О">{{ displayData.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎻忚堪">{{ displayData.description || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ displayData.updateByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeDictTypeListRow } from '../dictTypePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeDictTypeListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue b/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue
new file mode 100644
index 0000000..22abb74
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue
@@ -0,0 +1,154 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildDictTypeDialogModel, createDictTypeFormState } from '../dictTypePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ dictTypeData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createDictTypeFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫鏁版嵁瀛楀吀' : '鏂板鏁版嵁瀛楀吀'))
+
+ const rules = computed(() => ({
+ code: [{ required: true, message: '璇疯緭鍏ュ瓧鍏哥紪鐮�', trigger: 'blur' }],
+ name: [{ required: true, message: '璇疯緭鍏ュ瓧鍏稿悕绉�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '缂栫爜',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧鍏哥紪鐮�'
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧鍏稿悕绉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '鎻忚堪',
+ key: 'description',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ瓧鍏告弿杩�'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createDictTypeFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildDictTypeDialogModel(props.dictTypeData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.dictTypeData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/fields-item/fieldsItemPage.helpers.js b/rsf-design/src/views/system/fields-item/fieldsItemPage.helpers.js
new file mode 100644
index 0000000..75746c2
--- /dev/null
+++ b/rsf-design/src/views/system/fields-item/fieldsItemPage.helpers.js
@@ -0,0 +1,136 @@
+const STATUS_OPTIONS = [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+]
+
+export function createFieldsItemSearchState() {
+ return {
+ condition: '',
+ uuid: '',
+ fieldsId: '',
+ value: '',
+ matnrId: '',
+ shiperId: '',
+ status: ''
+ }
+}
+
+export function createFieldsItemFormState() {
+ return {
+ id: null,
+ uuid: '',
+ fieldsId: null,
+ value: '',
+ matnrId: null,
+ shiperId: null,
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getFieldsItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getFieldsItemStatusOptions() {
+ return STATUS_OPTIONS
+}
+
+export function getFieldsItemStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '姝e父', type: 'success', bool: true }
+ : { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export function buildFieldsItemSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ uuid: String(params.uuid || '').trim(),
+ ...(params.fieldsId !== '' && params.fieldsId !== null && params.fieldsId !== undefined
+ ? { fieldsId: Number(params.fieldsId) }
+ : {}),
+ value: String(params.value || '').trim(),
+ ...(params.matnrId !== '' && params.matnrId !== null && params.matnrId !== undefined
+ ? { matnrId: Number(params.matnrId) }
+ : {}),
+ ...(params.shiperId !== '' && params.shiperId !== null && params.shiperId !== undefined
+ ? { shiperId: Number(params.shiperId) }
+ : {}),
+ ...(params.status !== '' && params.status !== null && params.status !== undefined
+ ? { status: Number(params.status) }
+ : {})
+ }
+}
+
+export function buildFieldsItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildFieldsItemSearchParams(params)
+ }
+}
+
+export function buildFieldsItemDialogModel(record = {}) {
+ return {
+ ...createFieldsItemFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ uuid: record.uuid || '',
+ fieldsId:
+ record.fieldsId !== undefined && record.fieldsId !== null && record.fieldsId !== ''
+ ? Number(record.fieldsId)
+ : null,
+ value: record.value || '',
+ matnrId:
+ record.matnrId !== undefined && record.matnrId !== null && record.matnrId !== ''
+ ? Number(record.matnrId)
+ : null,
+ shiperId:
+ record.shiperId !== undefined && record.shiperId !== null && record.shiperId !== ''
+ ? Number(record.shiperId)
+ : null,
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildFieldsItemSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ uuid: String(formData.uuid || '').trim(),
+ ...(formData.fieldsId !== '' && formData.fieldsId !== null && formData.fieldsId !== undefined
+ ? { fieldsId: Number(formData.fieldsId) }
+ : {}),
+ value: String(formData.value || '').trim(),
+ ...(formData.matnrId !== '' && formData.matnrId !== null && formData.matnrId !== undefined
+ ? { matnrId: Number(formData.matnrId) }
+ : {}),
+ ...(formData.shiperId !== '' && formData.shiperId !== null && formData.shiperId !== undefined
+ ? { shiperId: Number(formData.shiperId) }
+ : {}),
+ status: Number(formData.status ?? 1),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function normalizeFieldsItemListRow(record = {}) {
+ const statusMeta = getFieldsItemStatusMeta(record.status)
+ return {
+ ...record,
+ uuid: record.uuid || '',
+ fieldsId: record.fieldsId ?? '',
+ value: record.value || '',
+ matnrId: record.matnrId ?? '',
+ shiperId: record.shiperId ?? '',
+ memo: record.memo || '',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateByLabel: record['updateBy$'] || record.updateBy || '',
+ createByLabel: record['createBy$'] || record.createBy || '',
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
diff --git a/rsf-design/src/views/system/fields-item/fieldsItemTable.columns.js b/rsf-design/src/views/system/fields-item/fieldsItemTable.columns.js
new file mode 100644
index 0000000..6a17e17
--- /dev/null
+++ b/rsf-design/src/views/system/fields-item/fieldsItemTable.columns.js
@@ -0,0 +1,80 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createFieldsItemTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'uuid',
+ label: '鍞竴鏍囪瘑',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'fieldsId',
+ label: '鎵╁睍瀛楁鏍囪瘑',
+ width: 130,
+ formatter: (row) => row.fieldsId || '-'
+ },
+ {
+ prop: 'value',
+ label: '瀛楁鍊�',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.value || '-'
+ },
+ {
+ prop: 'matnrId',
+ label: '鐗╂枡鏍囪瘑',
+ width: 120,
+ formatter: (row) => row.matnrId || '-'
+ },
+ {
+ prop: 'shiperId',
+ label: '璐т富鏍囪瘑',
+ width: 120,
+ formatter: (row) => row.shiperId || '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/fields-item/index.vue b/rsf-design/src/views/system/fields-item/index.vue
new file mode 100644
index 0000000..5773b73
--- /dev/null
+++ b/rsf-design/src/views/system/fields-item/index.vue
@@ -0,0 +1,232 @@
+<template>
+ <div class="fields-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鎵╁睍瀛楁鏄庣粏</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <FieldsItemDialog
+ v-model:visible="dialogVisible"
+ :fields-item-data="currentFieldsItemData"
+ @submit="handleDialogSubmit"
+ />
+
+ <FieldsItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import {
+ fetchDeleteFieldsItem,
+ fetchFieldsItemPage,
+ fetchGetFieldsItemDetail,
+ fetchSaveFieldsItem,
+ fetchUpdateFieldsItem
+ } from '@/api/system-manage'
+ import FieldsItemDialog from './modules/fields-item-dialog.vue'
+ import FieldsItemDetailDrawer from './modules/fields-item-detail-drawer.vue'
+ import { createFieldsItemTableColumns } from './fieldsItemTable.columns'
+ import {
+ buildFieldsItemDialogModel,
+ buildFieldsItemPageQueryParams,
+ buildFieldsItemSavePayload,
+ buildFieldsItemSearchParams,
+ createFieldsItemSearchState,
+ getFieldsItemPaginationKey,
+ getFieldsItemStatusOptions,
+ normalizeFieldsItemListRow
+ } from './fieldsItemPage.helpers'
+
+ defineOptions({ name: 'FieldsItem' })
+
+ const { hasAuth } = useAuth()
+ const searchForm = ref(createFieldsItemSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ敮涓�鏍囪瘑鎴栧瓧娈靛��'
+ }
+ },
+ {
+ label: '鍞竴鏍囪瘑',
+ key: 'uuid',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ敮涓�鏍囪瘑'
+ }
+ },
+ {
+ label: '鎵╁睍瀛楁鏍囪瘑',
+ key: 'fieldsId',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ墿灞曞瓧娈垫爣璇�'
+ }
+ },
+ {
+ label: '瀛楁鍊�',
+ key: 'value',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈靛��'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getFieldsItemStatusOptions()
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeFieldsItemListRow(await fetchGetFieldsItemDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鎵╁睍瀛楁鏄庣粏璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentFieldsItemData.value = buildFieldsItemDialogModel(await fetchGetFieldsItemDetail(row.id))
+ dialogVisible.value = true
+ dialogType.value = 'edit'
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇鎵╁睍瀛楁鏄庣粏璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchFieldsItemPage,
+ apiParams: buildFieldsItemPageQueryParams(searchForm.value),
+ paginationKey: getFieldsItemPaginationKey(),
+ columnsFactory: () =>
+ createFieldsItemTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeFieldsItemListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentFieldsItemData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildFieldsItemDialogModel(),
+ buildEditModel: (record) => buildFieldsItemDialogModel(record),
+ buildSavePayload: (formData) => buildFieldsItemSavePayload(formData),
+ saveRequest: fetchSaveFieldsItem,
+ updateRequest: fetchUpdateFieldsItem,
+ deleteRequest: fetchDeleteFieldsItem,
+ entityName: '鎵╁睍瀛楁鏄庣粏',
+ resolveRecordLabel: (record) => record?.uuid || record?.value || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ function handleSearch(params) {
+ replaceSearchParams(buildFieldsItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createFieldsItemSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/fields-item/modules/fields-item-detail-drawer.vue b/rsf-design/src/views/system/fields-item/modules/fields-item-detail-drawer.vue
new file mode 100644
index 0000000..673076e
--- /dev/null
+++ b/rsf-design/src/views/system/fields-item/modules/fields-item-detail-drawer.vue
@@ -0,0 +1,52 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鎵╁睍瀛楁鏄庣粏璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="鍞竴鏍囪瘑">{{ displayData.uuid || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵╁睍瀛楁鏍囪瘑">{{ displayData.fieldsId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁鍊�">{{ displayData.value || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鏍囪瘑">{{ displayData.matnrId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璐т富鏍囪瘑">{{ displayData.shiperId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ displayData.updateByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeFieldsItemListRow } from '../fieldsItemPage.helpers'
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ default: false
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ detailData: {
+ type: Object,
+ default: () => ({})
+ }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeFieldsItemListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/fields-item/modules/fields-item-dialog.vue b/rsf-design/src/views/system/fields-item/modules/fields-item-dialog.vue
new file mode 100644
index 0000000..486f286
--- /dev/null
+++ b/rsf-design/src/views/system/fields-item/modules/fields-item-dialog.vue
@@ -0,0 +1,174 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildFieldsItemDialogModel,
+ createFieldsItemFormState,
+ getFieldsItemStatusOptions
+ } from '../fieldsItemPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ fieldsItemData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createFieldsItemFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫鎵╁睍瀛楁鏄庣粏' : '鏂板鎵╁睍瀛楁鏄庣粏'))
+
+ const rules = computed(() => ({
+ uuid: [{ required: true, message: '璇疯緭鍏ュ敮涓�鏍囪瘑', trigger: 'blur' }],
+ fieldsId: [{ required: true, message: '璇疯緭鍏ユ墿灞曞瓧娈垫爣璇�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '鍞竴鏍囪瘑',
+ key: 'uuid',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ敮涓�鏍囪瘑'
+ }
+ },
+ {
+ label: '鎵╁睍瀛楁鏍囪瘑',
+ key: 'fieldsId',
+ type: 'input-number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ墿灞曞瓧娈垫爣璇�'
+ }
+ },
+ {
+ label: '瀛楁鍊�',
+ key: 'value',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈靛��'
+ }
+ },
+ {
+ label: '鐗╂枡鏍囪瘑',
+ key: 'matnrId',
+ type: 'input-number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ョ墿鏂欐爣璇�'
+ }
+ },
+ {
+ label: '璐т富鏍囪瘑',
+ key: 'shiperId',
+ type: 'input-number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ揣涓绘爣璇�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: getFieldsItemStatusOptions(),
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createFieldsItemFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildFieldsItemDialogModel(props.fieldsItemData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.fieldsItemData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/fields/fieldsPage.helpers.js b/rsf-design/src/views/system/fields/fieldsPage.helpers.js
new file mode 100644
index 0000000..37b3043
--- /dev/null
+++ b/rsf-design/src/views/system/fields/fieldsPage.helpers.js
@@ -0,0 +1,134 @@
+const UNIQUE_OPTIONS = [
+ { label: '闈炲繀濉�', value: 0, text: '涓绘暟鎹�' },
+ { label: '蹇呭~', value: 1, text: '涓氬姟鏁版嵁' }
+]
+
+const ENABLE_OPTIONS = [
+ { label: '涓嶅惎鐢�', value: 0, text: '涓嶅惎鐢�' },
+ { label: '鍚敤', value: 1, text: '鍚敤' }
+]
+
+export function createFieldsSearchState() {
+ return {
+ condition: '',
+ fields: '',
+ fieldsAlise: '',
+ unique: '',
+ flagEnable: '',
+ status: ''
+ }
+}
+
+export function createFieldsFormState() {
+ return {
+ id: null,
+ fields: '',
+ fieldsAlise: '',
+ unique: 0,
+ flagEnable: 1,
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getFieldsPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getFieldsUniqueOptions() {
+ return UNIQUE_OPTIONS
+}
+
+export function getFieldsEnableOptions() {
+ return ENABLE_OPTIONS
+}
+
+export function getFieldsStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '姝e父', type: 'success', bool: true }
+ : { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export function getFieldsUniqueMeta(unique) {
+ const normalized = Number(unique)
+ return UNIQUE_OPTIONS.find((item) => item.value === normalized) || { label: '-', value: normalized, text: '-' }
+}
+
+export function getFieldsEnableMeta(flagEnable) {
+ const normalized = Number(flagEnable)
+ return ENABLE_OPTIONS.find((item) => item.value === normalized) || { label: '-', value: normalized, text: '-' }
+}
+
+export function buildFieldsSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ fields: String(params.fields || '').trim(),
+ fieldsAlise: String(params.fieldsAlise || '').trim(),
+ ...(params.unique !== '' && params.unique !== null && params.unique !== undefined
+ ? { unique: Number(params.unique) }
+ : {}),
+ ...(params.flagEnable !== '' && params.flagEnable !== null && params.flagEnable !== undefined
+ ? { flagEnable: Number(params.flagEnable) }
+ : {}),
+ ...(params.status !== '' && params.status !== null && params.status !== undefined
+ ? { status: Number(params.status) }
+ : {})
+ }
+}
+
+export function buildFieldsPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildFieldsSearchParams(params)
+ }
+}
+
+export function buildFieldsDialogModel(record = {}) {
+ return {
+ ...createFieldsFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ fields: record.fields || '',
+ fieldsAlise: record.fieldsAlise || '',
+ unique: record.unique !== undefined && record.unique !== null ? Number(record.unique) : 0,
+ flagEnable: record.flagEnable !== undefined && record.flagEnable !== null ? Number(record.flagEnable) : 1,
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildFieldsSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ fields: String(formData.fields || '').trim(),
+ fieldsAlise: String(formData.fieldsAlise || '').trim(),
+ unique: Number(formData.unique ?? 0),
+ flagEnable: Number(formData.flagEnable ?? 1),
+ status: Number(formData.status ?? 1),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function normalizeFieldsListRow(record = {}) {
+ const statusMeta = getFieldsStatusMeta(record.status)
+ const uniqueMeta = getFieldsUniqueMeta(record.unique)
+ const enableMeta = getFieldsEnableMeta(record.flagEnable)
+ return {
+ ...record,
+ fields: record.fields || '',
+ fieldsAlise: record.fieldsAlise || '',
+ memo: record.memo || '',
+ uniqueText: record['unique$'] || uniqueMeta.text,
+ flagEnableText: record['flagEnable$'] || enableMeta.text,
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateByLabel: record['updateBy$'] || record.updateByLabel || '',
+ createByLabel: record['createBy$'] || record.createByLabel || '',
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
diff --git a/rsf-design/src/views/system/fields/fieldsTable.columns.js b/rsf-design/src/views/system/fields/fieldsTable.columns.js
new file mode 100644
index 0000000..9e45e5d
--- /dev/null
+++ b/rsf-design/src/views/system/fields/fieldsTable.columns.js
@@ -0,0 +1,73 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createFieldsTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'fields',
+ label: '瀛楁鍚�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'fieldsAlise',
+ label: '瀛楁鍒悕',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'uniqueText',
+ label: '瀛楁灞炴��',
+ width: 120,
+ formatter: (row) => row.uniqueText || '-'
+ },
+ {
+ prop: 'flagEnableText',
+ label: '鍚敤鐘舵��',
+ width: 120,
+ formatter: (row) => row.flagEnableText || '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/fields/index.vue b/rsf-design/src/views/system/fields/index.vue
new file mode 100644
index 0000000..8a23c68
--- /dev/null
+++ b/rsf-design/src/views/system/fields/index.vue
@@ -0,0 +1,233 @@
+<template>
+ <div class="fields-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鎵╁睍瀛楁</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <FieldsDialog
+ v-model:visible="dialogVisible"
+ :fields-data="currentFieldsData"
+ @submit="handleDialogSubmit"
+ />
+
+ <FieldsDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import {
+ fetchDeleteFields,
+ fetchFieldsPage,
+ fetchGetFieldsDetail,
+ fetchSaveFields,
+ fetchUpdateFields
+ } from '@/api/system-manage'
+ import FieldsDialog from './modules/fields-dialog.vue'
+ import FieldsDetailDrawer from './modules/fields-detail-drawer.vue'
+ import { createFieldsTableColumns } from './fieldsTable.columns'
+ import {
+ buildFieldsDialogModel,
+ buildFieldsPageQueryParams,
+ buildFieldsSavePayload,
+ buildFieldsSearchParams,
+ createFieldsSearchState,
+ getFieldsEnableOptions,
+ getFieldsPaginationKey,
+ getFieldsUniqueOptions,
+ normalizeFieldsListRow
+ } from './fieldsPage.helpers'
+
+ defineOptions({ name: 'Fields' })
+
+ const { hasAuth } = useAuth()
+ const searchForm = ref(createFieldsSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈靛悕鎴栧埆鍚�'
+ }
+ },
+ {
+ label: '瀛楁鍚�',
+ key: 'fields',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈靛悕'
+ }
+ },
+ {
+ label: '瀛楁鍒悕',
+ key: 'fieldsAlise',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈靛埆鍚�'
+ }
+ },
+ {
+ label: '瀛楁灞炴��',
+ key: 'unique',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getFieldsUniqueOptions()
+ }
+ },
+ {
+ label: '鍚敤鐘舵��',
+ key: 'flagEnable',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getFieldsEnableOptions()
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeFieldsListRow(await fetchGetFieldsDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鎵╁睍瀛楁璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentFieldsData.value = buildFieldsDialogModel(await fetchGetFieldsDetail(row.id))
+ dialogVisible.value = true
+ dialogType.value = 'edit'
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇鎵╁睍瀛楁璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchFieldsPage,
+ apiParams: buildFieldsPageQueryParams(searchForm.value),
+ paginationKey: getFieldsPaginationKey(),
+ columnsFactory: () =>
+ createFieldsTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeFieldsListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentFieldsData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildFieldsDialogModel(),
+ buildEditModel: (record) => buildFieldsDialogModel(record),
+ buildSavePayload: (formData) => buildFieldsSavePayload(formData),
+ saveRequest: fetchSaveFields,
+ updateRequest: fetchUpdateFields,
+ deleteRequest: fetchDeleteFields,
+ entityName: '鎵╁睍瀛楁',
+ resolveRecordLabel: (record) => record?.fieldsAlise || record?.fields || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ function handleSearch(params) {
+ replaceSearchParams(buildFieldsSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createFieldsSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/fields/modules/fields-detail-drawer.vue b/rsf-design/src/views/system/fields/modules/fields-detail-drawer.vue
new file mode 100644
index 0000000..3922ac4
--- /dev/null
+++ b/rsf-design/src/views/system/fields/modules/fields-detail-drawer.vue
@@ -0,0 +1,42 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鎵╁睍瀛楁璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="瀛楁鍚�">{{ displayData.fields || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁鍒悕">{{ displayData.fieldsAlise || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁灞炴��">{{ displayData.uniqueText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚敤鐘舵��">{{ displayData.flagEnableText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ displayData.updateByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeFieldsListRow } from '../fieldsPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeFieldsListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/fields/modules/fields-dialog.vue b/rsf-design/src/views/system/fields/modules/fields-dialog.vue
new file mode 100644
index 0000000..ec28062
--- /dev/null
+++ b/rsf-design/src/views/system/fields/modules/fields-dialog.vue
@@ -0,0 +1,166 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildFieldsDialogModel,
+ createFieldsFormState,
+ getFieldsEnableOptions,
+ getFieldsUniqueOptions
+ } from '../fieldsPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ fieldsData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createFieldsFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫鎵╁睍瀛楁' : '鏂板鎵╁睍瀛楁'))
+
+ const rules = computed(() => ({
+ fields: [{ required: true, message: '璇疯緭鍏ュ瓧娈靛悕', trigger: 'blur' }],
+ fieldsAlise: [{ required: true, message: '璇疯緭鍏ュ瓧娈靛埆鍚�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '瀛楁鍚�',
+ key: 'fields',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈靛悕'
+ }
+ },
+ {
+ label: '瀛楁鍒悕',
+ key: 'fieldsAlise',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈靛埆鍚�'
+ }
+ },
+ {
+ label: '瀛楁灞炴��',
+ key: 'unique',
+ type: 'select',
+ props: {
+ options: getFieldsUniqueOptions(),
+ placeholder: '璇烽�夋嫨瀛楁灞炴��'
+ }
+ },
+ {
+ label: '鍚敤鐘舵��',
+ key: 'flagEnable',
+ type: 'select',
+ props: {
+ options: getFieldsEnableOptions(),
+ placeholder: '璇烽�夋嫨鍚敤鐘舵��'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createFieldsFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildFieldsDialogModel(props.fieldsData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.fieldsData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/flow-instance/flowInstancePage.helpers.js b/rsf-design/src/views/system/flow-instance/flowInstancePage.helpers.js
new file mode 100644
index 0000000..e46da7e
--- /dev/null
+++ b/rsf-design/src/views/system/flow-instance/flowInstancePage.helpers.js
@@ -0,0 +1,175 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const FLOW_INSTANCE_REPORT_TITLE = '娴佺▼瀹炰緥鎶ヨ〃'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function normalizeDateTime(value) {
+ return normalizeText(value) || '--'
+}
+
+export function createFlowInstanceSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ flowInstanceNo: '',
+ taskId: '',
+ taskNo: '',
+ nodeInstanceId: '',
+ nodeCode: '',
+ flowTemplateId: '',
+ flowTemplateCode: '',
+ templateVersion: '',
+ currentStepCode: '',
+ currentStepOrder: '',
+ executeResult: '',
+ errorCode: '',
+ errorMessage: '',
+ startTime: '',
+ endTime: '',
+ timeoutAt: '',
+ durationSeconds: '',
+ retryTimes: '',
+ lastRetryTime: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getFlowInstancePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildFlowInstanceSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'flowInstanceNo',
+ 'taskNo',
+ 'nodeCode',
+ 'flowTemplateCode',
+ 'currentStepCode',
+ 'executeResult',
+ 'errorCode',
+ 'errorMessage',
+ 'startTime',
+ 'endTime',
+ 'timeoutAt',
+ 'lastRetryTime',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;[
+ 'taskId',
+ 'nodeInstanceId',
+ 'flowTemplateId',
+ 'templateVersion',
+ 'currentStepOrder',
+ 'durationSeconds',
+ 'retryTimes',
+ 'status'
+ ].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildFlowInstancePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildFlowInstanceSearchParams(params)
+ }
+}
+
+export function normalizeFlowInstanceRow(record = {}) {
+ const statusMeta = STATUS_META[Number(record.status)] || STATUS_META[0]
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ flowInstanceNo: normalizeText(record.flowInstanceNo) || '--',
+ taskNo: normalizeText(record.taskNo) || '--',
+ nodeCode: normalizeText(record.nodeCode) || '--',
+ flowTemplateCode: normalizeText(record.flowTemplateCode) || '--',
+ currentStepCode: normalizeText(record.currentStepCode) || '--',
+ executeResult: normalizeText(record.executeResult) || '--',
+ errorMessage: normalizeText(record.errorMessage) || '--',
+ startTimeText: normalizeDateTime(record['startTime$'] || record.startTimeText || record.startTime),
+ endTimeText: normalizeDateTime(record['endTime$'] || record.endTimeText || record.endTime),
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function getFlowInstanceReportColumns() {
+ return [
+ { prop: 'flowInstanceNo', label: '娴佺▼瀹炰緥鍙�' },
+ { prop: 'taskNo', label: '浠诲姟鍙�' },
+ { prop: 'nodeCode', label: '鑺傜偣缂栫爜' },
+ { prop: 'flowTemplateCode', label: '娴佺▼妯℃澘缂栫爜' },
+ { prop: 'currentStepCode', label: '褰撳墠姝ラ' },
+ { prop: 'executeResult', label: '鎵ц缁撴灉' },
+ { prop: 'errorMessage', label: '閿欒淇℃伅' },
+ { prop: 'startTimeText', label: '寮�濮嬫椂闂�' },
+ { prop: 'endTimeText', label: '缁撴潫鏃堕棿' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildFlowInstancePrintRows(records = []) {
+ return (Array.isArray(records) ? records : []).map((record) => {
+ const row = normalizeFlowInstanceRow(record)
+ return {
+ flowInstanceNo: row.flowInstanceNo,
+ taskNo: row.taskNo,
+ nodeCode: row.nodeCode,
+ flowTemplateCode: row.flowTemplateCode,
+ currentStepCode: row.currentStepCode,
+ executeResult: row.executeResult,
+ errorMessage: row.errorMessage,
+ startTimeText: row.startTimeText,
+ endTimeText: row.endTimeText,
+ statusText: row.statusText,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/flow-instance/flowInstanceTable.columns.js b/rsf-design/src/views/system/flow-instance/flowInstanceTable.columns.js
new file mode 100644
index 0000000..58785d0
--- /dev/null
+++ b/rsf-design/src/views/system/flow-instance/flowInstanceTable.columns.js
@@ -0,0 +1,35 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createFlowInstanceTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'flowInstanceNo', label: '娴佺▼瀹炰緥鍙�', minWidth: 180, showOverflowTooltip: true },
+ { prop: 'taskNo', label: '浠诲姟鍙�', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'nodeCode', label: '鑺傜偣缂栫爜', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'flowTemplateCode', label: '娴佺▼妯℃澘缂栫爜', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'currentStepCode', label: '褰撳墠姝ラ', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'executeResult', label: '鎵ц缁撴灉', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'startTimeText', label: '寮�濮嬫椂闂�', minWidth: 170, showOverflowTooltip: true },
+ { prop: 'endTimeText', label: '缁撴潫鏃堕棿', minWidth: 170, showOverflowTooltip: true },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.statusType || 'info', effect: 'light' }, () => row?.statusText || '--')
+ },
+ { prop: 'memo', label: '澶囨敞', minWidth: 180, showOverflowTooltip: true },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) => h(ArtButtonTable, { icon: 'ri:eye-line', onClick: () => handleView?.(row) })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/flow-instance/index.vue b/rsf-design/src/views/system/flow-instance/index.vue
new file mode 100644
index 0000000..5e0851c
--- /dev/null
+++ b/rsf-design/src/views/system/flow-instance/index.vue
@@ -0,0 +1,117 @@
+<template>
+ <div class="flow-instance-page art-full-height">
+ <ArtSearchBar v-model="searchForm" :items="searchItems" :showExpand="true" @search="handleSearch" @reset="handleReset" />
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+ <FlowInstanceDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { fetchExportFlowInstanceReport, fetchFlowInstancePage, fetchGetFlowInstanceDetail, fetchGetFlowInstanceMany } from '@/api/flow-instance'
+ import { buildFlowInstancePageQueryParams, buildFlowInstancePrintRows, buildFlowInstanceSearchParams, createFlowInstanceSearchState, getFlowInstancePaginationKey, getFlowInstanceReportColumns, normalizeFlowInstanceRow, FLOW_INSTANCE_REPORT_TITLE } from './flowInstancePage.helpers'
+ import { createFlowInstanceTableColumns } from './flowInstanceTable.columns'
+ import FlowInstanceDetailDrawer from './modules/flow-instance-detail-drawer.vue'
+
+ defineOptions({ name: 'FlowInstance' })
+ const userStore = useUserStore()
+ const searchForm = ref(createFlowInstanceSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = FLOW_INSTANCE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildFlowInstanceSearchParams(searchForm.value))
+ const reportColumns = getFlowInstanceReportColumns()
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ュ疄渚嬪彿/浠诲姟鍙�/鑺傜偣缂栫爜' } },
+ { label: '娴佺▼瀹炰緥鍙�', key: 'flowInstanceNo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋嬪疄渚嬪彿' } },
+ { label: '浠诲姟鍙�', key: 'taskNo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヤ换鍔″彿' } },
+ { label: '鑺傜偣缂栫爜', key: 'nodeCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ妭鐐圭紪鐮�' } },
+ { label: '寮�濮嬫棩鏈�', key: 'timeStart', type: 'date', props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' } },
+ { label: '缁撴潫鏃ユ湡', key: 'timeEnd', type: 'date', props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' } }
+ ])
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData } =
+ useTable({
+ core: {
+ apiFn: fetchFlowInstancePage,
+ apiParams: buildFlowInstancePageQueryParams(searchForm.value),
+ paginationKey: getFlowInstancePaginationKey(),
+ columnsFactory: () => createFlowInstanceTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeFlowInstanceRow(item)) : [])
+ }
+ })
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetFlowInstanceMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(await fetchFlowInstancePage({ ...reportQueryParams.value, current: 1, pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20 })).records
+ }
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } = usePrintExportPage({
+ downloadFileName: 'flow-instance.xlsx',
+ requestExport: (payload) => fetchExportFlowInstanceReport(payload, { headers: { Authorization: userStore.accessToken || '' } }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildFlowInstancePrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetFlowInstanceDetail(id)
+ detailData.value = normalizeFlowInstanceRow({ ...fallback, ...detail })
+ }
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+ function handleSearch(params) {
+ replaceSearchParams(buildFlowInstanceSearchParams(params))
+ getData()
+ }
+ function handleReset() {
+ Object.assign(searchForm.value, createFlowInstanceSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/flow-instance/modules/flow-instance-detail-drawer.vue b/rsf-design/src/views/system/flow-instance/modules/flow-instance-detail-drawer.vue
new file mode 100644
index 0000000..746b55d
--- /dev/null
+++ b/rsf-design/src/views/system/flow-instance/modules/flow-instance-detail-drawer.vue
@@ -0,0 +1,35 @@
+<template>
+ <ElDrawer :model-value="visible" title="娴佺▼瀹炰緥璇︽儏" size="72%" @update:model-value="handleVisibleChange">
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="娴佺▼瀹炰緥鍙�">{{ detail.flowInstanceNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鍙�">{{ detail.taskNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣缂栫爜">{{ detail.nodeCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="娴佺▼妯℃澘缂栫爜">{{ detail.flowTemplateCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="褰撳墠姝ラ">{{ detail.currentStepCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц缁撴灉" :span="2">{{ detail.executeResult || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿欒淇℃伅" :span="2">{{ detail.errorMessage || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寮�濮嬫椂闂�">{{ detail.startTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁撴潫鏃堕棿">{{ detail.endTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'FlowInstanceDetailDrawer' })
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+ const emit = defineEmits(['update:visible'])
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/flow-step-instance/flowStepInstancePage.helpers.js b/rsf-design/src/views/system/flow-step-instance/flowStepInstancePage.helpers.js
new file mode 100644
index 0000000..4339685
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-instance/flowStepInstancePage.helpers.js
@@ -0,0 +1,162 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const FLOW_STEP_INSTANCE_REPORT_TITLE = '娴佺▼姝ラ瀹炰緥鎶ヨ〃'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function normalizeDateTime(value) {
+ return normalizeText(value) || '--'
+}
+
+export function createFlowStepInstanceSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ flowInstanceId: '',
+ flowInstanceNo: '',
+ stepOrder: '',
+ stepCode: '',
+ stepName: '',
+ stepType: '',
+ stepTemplateId: '',
+ executeResult: '',
+ errorCode: '',
+ errorMessage: '',
+ startTime: '',
+ endTime: '',
+ durationSeconds: '',
+ inputData: '',
+ outputData: '',
+ retryTimes: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getFlowStepInstancePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildFlowStepInstanceSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'flowInstanceNo',
+ 'stepCode',
+ 'stepName',
+ 'stepType',
+ 'executeResult',
+ 'errorCode',
+ 'errorMessage',
+ 'startTime',
+ 'endTime',
+ 'inputData',
+ 'outputData',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;['flowInstanceId', 'stepOrder', 'stepTemplateId', 'durationSeconds', 'retryTimes', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildFlowStepInstancePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildFlowStepInstanceSearchParams(params)
+ }
+}
+
+export function normalizeFlowStepInstanceRow(record = {}) {
+ const statusMeta = STATUS_META[Number(record.status)] || STATUS_META[0]
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ flowInstanceNo: normalizeText(record.flowInstanceNo) || '--',
+ stepCode: normalizeText(record.stepCode) || '--',
+ stepName: normalizeText(record.stepName) || '--',
+ stepType: normalizeText(record.stepType) || '--',
+ executeResult: normalizeText(record.executeResult) || '--',
+ errorMessage: normalizeText(record.errorMessage) || '--',
+ startTimeText: normalizeDateTime(record['startTime$'] || record.startTimeText || record.startTime),
+ endTimeText: normalizeDateTime(record['endTime$'] || record.endTimeText || record.endTime),
+ durationSeconds: record.durationSeconds ?? '--',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function getFlowStepInstanceReportColumns() {
+ return [
+ { prop: 'flowInstanceNo', label: '娴佺▼瀹炰緥鍙�' },
+ { prop: 'stepCode', label: '姝ラ缂栫爜' },
+ { prop: 'stepName', label: '姝ラ鍚嶇О' },
+ { prop: 'stepType', label: '姝ラ绫诲瀷' },
+ { prop: 'executeResult', label: '鎵ц缁撴灉' },
+ { prop: 'errorMessage', label: '閿欒淇℃伅' },
+ { prop: 'startTimeText', label: '寮�濮嬫椂闂�' },
+ { prop: 'endTimeText', label: '缁撴潫鏃堕棿' },
+ { prop: 'durationSeconds', label: '鑰楁椂(绉�)' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildFlowStepInstancePrintRows(records = []) {
+ return (Array.isArray(records) ? records : []).map((record) => {
+ const row = normalizeFlowStepInstanceRow(record)
+ return {
+ flowInstanceNo: row.flowInstanceNo,
+ stepCode: row.stepCode,
+ stepName: row.stepName,
+ stepType: row.stepType,
+ executeResult: row.executeResult,
+ errorMessage: row.errorMessage,
+ startTimeText: row.startTimeText,
+ endTimeText: row.endTimeText,
+ durationSeconds: row.durationSeconds,
+ statusText: row.statusText,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/flow-step-instance/flowStepInstanceTable.columns.js b/rsf-design/src/views/system/flow-step-instance/flowStepInstanceTable.columns.js
new file mode 100644
index 0000000..967c4bc
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-instance/flowStepInstanceTable.columns.js
@@ -0,0 +1,35 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createFlowStepInstanceTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'flowInstanceNo', label: '娴佺▼瀹炰緥鍙�', minWidth: 180, showOverflowTooltip: true },
+ { prop: 'stepCode', label: '姝ラ缂栫爜', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'stepName', label: '姝ラ鍚嶇О', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'stepType', label: '姝ラ绫诲瀷', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'executeResult', label: '鎵ц缁撴灉', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'startTimeText', label: '寮�濮嬫椂闂�', minWidth: 170, showOverflowTooltip: true },
+ { prop: 'endTimeText', label: '缁撴潫鏃堕棿', minWidth: 170, showOverflowTooltip: true },
+ { prop: 'durationSeconds', label: '鑰楁椂(绉�)', width: 110, align: 'right' },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(ElTag, { type: row?.statusType || 'info', effect: 'light' }, () => row?.statusText || '--')
+ },
+ { prop: 'memo', label: '澶囨敞', minWidth: 180, showOverflowTooltip: true },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) => h(ArtButtonTable, { icon: 'ri:eye-line', onClick: () => handleView?.(row) })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/flow-step-instance/index.vue b/rsf-design/src/views/system/flow-step-instance/index.vue
new file mode 100644
index 0000000..22e2cbb
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-instance/index.vue
@@ -0,0 +1,117 @@
+<template>
+ <div class="flow-step-instance-page art-full-height">
+ <ArtSearchBar v-model="searchForm" :items="searchItems" :showExpand="true" @search="handleSearch" @reset="handleReset" />
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+ <FlowStepInstanceDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { fetchExportFlowStepInstanceReport, fetchFlowStepInstancePage, fetchGetFlowStepInstanceDetail, fetchGetFlowStepInstanceMany } from '@/api/flow-step-instance'
+ import { buildFlowStepInstancePageQueryParams, buildFlowStepInstancePrintRows, buildFlowStepInstanceSearchParams, createFlowStepInstanceSearchState, getFlowStepInstancePaginationKey, getFlowStepInstanceReportColumns, normalizeFlowStepInstanceRow, FLOW_STEP_INSTANCE_REPORT_TITLE } from './flowStepInstancePage.helpers'
+ import { createFlowStepInstanceTableColumns } from './flowStepInstanceTable.columns'
+ import FlowStepInstanceDetailDrawer from './modules/flow-step-instance-detail-drawer.vue'
+
+ defineOptions({ name: 'FlowStepInstance' })
+ const userStore = useUserStore()
+ const searchForm = ref(createFlowStepInstanceSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = FLOW_STEP_INSTANCE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildFlowStepInstanceSearchParams(searchForm.value))
+ const reportColumns = getFlowStepInstanceReportColumns()
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋嬪疄渚嬪彿/姝ラ缂栫爜/姝ラ鍚嶇О' } },
+ { label: '娴佺▼瀹炰緥鍙�', key: 'flowInstanceNo', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋嬪疄渚嬪彿' } },
+ { label: '姝ラ缂栫爜', key: 'stepCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ楠ょ紪鐮�' } },
+ { label: '姝ラ鍚嶇О', key: 'stepName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ楠ゅ悕绉�' } },
+ { label: '寮�濮嬫棩鏈�', key: 'timeStart', type: 'date', props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' } },
+ { label: '缁撴潫鏃ユ湡', key: 'timeEnd', type: 'date', props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' } }
+ ])
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+ const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData } =
+ useTable({
+ core: {
+ apiFn: fetchFlowStepInstancePage,
+ apiParams: buildFlowStepInstancePageQueryParams(searchForm.value),
+ paginationKey: getFlowStepInstancePaginationKey(),
+ columnsFactory: () => createFlowStepInstanceTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeFlowStepInstanceRow(item)) : [])
+ }
+ })
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetFlowStepInstanceMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(await fetchFlowStepInstancePage({ ...reportQueryParams.value, current: 1, pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20 })).records
+ }
+ const { previewVisible, previewRows, previewMeta, handlePreviewVisibleChange, handleExport, handlePrint } = usePrintExportPage({
+ downloadFileName: 'flow-step-instance.xlsx',
+ requestExport: (payload) => fetchExportFlowStepInstanceReport(payload, { headers: { Authorization: userStore.accessToken || '' } }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildFlowStepInstancePrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetFlowStepInstanceDetail(id)
+ detailData.value = normalizeFlowStepInstanceRow({ ...fallback, ...detail })
+ }
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+ function handleSearch(params) {
+ replaceSearchParams(buildFlowStepInstanceSearchParams(params))
+ getData()
+ }
+ function handleReset() {
+ Object.assign(searchForm.value, createFlowStepInstanceSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/flow-step-instance/modules/flow-step-instance-detail-drawer.vue b/rsf-design/src/views/system/flow-step-instance/modules/flow-step-instance-detail-drawer.vue
new file mode 100644
index 0000000..da8e5f9
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-instance/modules/flow-step-instance-detail-drawer.vue
@@ -0,0 +1,40 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="娴佺▼姝ラ瀹炰緥璇︽儏"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="娴佺▼瀹炰緥鍙�">{{ detail.flowInstanceNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ラ缂栫爜">{{ detail.stepCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ラ鍚嶇О">{{ detail.stepName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ラ绫诲瀷">{{ detail.stepType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц缁撴灉" :span="2">{{ detail.executeResult || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿欒淇℃伅" :span="2">{{ detail.errorMessage || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寮�濮嬫椂闂�">{{ detail.startTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁撴潫鏃堕棿">{{ detail.endTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑰楁椂(绉�)">{{ detail.durationSeconds ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'FlowStepInstanceDetailDrawer' })
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+ const emit = defineEmits(['update:visible'])
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/flow-step-log/flowStepLogPage.helpers.js b/rsf-design/src/views/system/flow-step-log/flowStepLogPage.helpers.js
new file mode 100644
index 0000000..0cc9313
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-log/flowStepLogPage.helpers.js
@@ -0,0 +1,121 @@
+export const FLOW_STEP_LOG_REPORT_TITLE = '娴佺▼姝ラ鏃ュ織鎶ヨ〃'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+export function createFlowStepLogSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ flowInstanceId: '',
+ stepInstanceId: '',
+ logType: '',
+ logLevel: '',
+ logContent: '',
+ requestData: '',
+ responseData: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getFlowStepLogPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildFlowStepLogSearchParams(params = {}) {
+ const result = {}
+
+ ;['condition', 'logType', 'logLevel', 'logContent', 'requestData', 'responseData', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;['flowInstanceId', 'stepInstanceId', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildFlowStepLogPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildFlowStepLogSearchParams(params)
+ }
+}
+
+export function normalizeFlowStepLogRow(record = {}) {
+ return {
+ ...record,
+ id: record.id ?? '--',
+ flowInstanceId: record.flowInstanceId ?? '--',
+ stepInstanceId: record.stepInstanceId ?? '--',
+ logType: normalizeText(record.logType) || '--',
+ logLevel: normalizeText(record.logLevel) || '--',
+ logContent: normalizeText(record.logContent) || '--',
+ requestData: normalizeText(record.requestData) || '--',
+ responseData: normalizeText(record.responseData) || '--',
+ createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function getFlowStepLogReportColumns() {
+ return [
+ { prop: 'flowInstanceId', label: '娴佺▼瀹炰緥ID' },
+ { prop: 'stepInstanceId', label: '姝ラ瀹炰緥ID' },
+ { prop: 'logType', label: '鏃ュ織绫诲瀷' },
+ { prop: 'logLevel', label: '鏃ュ織绾у埆' },
+ { prop: 'logContent', label: '鏃ュ織鍐呭' },
+ { prop: 'requestData', label: '璇锋眰鏁版嵁' },
+ { prop: 'responseData', label: '鍝嶅簲鏁版嵁' },
+ { prop: 'createTimeText', label: '鍒涘缓鏃堕棿' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildFlowStepLogPrintRows(records = []) {
+ return (Array.isArray(records) ? records : []).map((record) => {
+ const row = normalizeFlowStepLogRow(record)
+ return {
+ flowInstanceId: row.flowInstanceId,
+ stepInstanceId: row.stepInstanceId,
+ logType: row.logType,
+ logLevel: row.logLevel,
+ logContent: row.logContent,
+ requestData: row.requestData,
+ responseData: row.responseData,
+ createTimeText: row.createTimeText,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/flow-step-log/flowStepLogTable.columns.js b/rsf-design/src/views/system/flow-step-log/flowStepLogTable.columns.js
new file mode 100644
index 0000000..74b2969
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-log/flowStepLogTable.columns.js
@@ -0,0 +1,75 @@
+import { h } from 'vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createFlowStepLogTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'flowInstanceId',
+ label: '娴佺▼瀹炰緥ID',
+ minWidth: 120,
+ align: 'right'
+ },
+ {
+ prop: 'stepInstanceId',
+ label: '姝ラ瀹炰緥ID',
+ minWidth: 120,
+ align: 'right'
+ },
+ {
+ prop: 'logType',
+ label: '鏃ュ織绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'logLevel',
+ label: '鏃ュ織绾у埆',
+ width: 110,
+ align: 'center'
+ },
+ {
+ prop: 'logContent',
+ label: '鏃ュ織鍐呭',
+ minWidth: 260,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'requestData',
+ label: '璇锋眰鏁版嵁',
+ minWidth: 240,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'responseData',
+ label: '鍝嶅簲鏁版嵁',
+ minWidth: 240,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/flow-step-log/index.vue b/rsf-design/src/views/system/flow-step-log/index.vue
new file mode 100644
index 0000000..c51dab1
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-log/index.vue
@@ -0,0 +1,244 @@
+<template>
+ <div class="flow-step-log-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <FlowStepLogDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ fetchExportFlowStepLogReport,
+ fetchFlowStepLogPage,
+ fetchGetFlowStepLogDetail,
+ fetchGetFlowStepLogMany
+ } from '@/api/flow-step-log'
+ import {
+ buildFlowStepLogPageQueryParams,
+ buildFlowStepLogPrintRows,
+ buildFlowStepLogSearchParams,
+ createFlowStepLogSearchState,
+ getFlowStepLogPaginationKey,
+ getFlowStepLogReportColumns,
+ normalizeFlowStepLogRow,
+ FLOW_STEP_LOG_REPORT_TITLE
+ } from './flowStepLogPage.helpers'
+ import { createFlowStepLogTableColumns } from './flowStepLogTable.columns'
+ import FlowStepLogDetailDrawer from './modules/flow-step-log-detail-drawer.vue'
+
+ defineOptions({ name: 'FlowStepLog' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createFlowStepLogSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = FLOW_STEP_LOG_REPORT_TITLE
+ const reportQueryParams = computed(() => buildFlowStepLogSearchParams(searchForm.value))
+ const reportColumns = getFlowStepLogReportColumns()
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ祦绋嬪疄渚婭D/姝ラ瀹炰緥ID/鏃ュ織鍐呭'
+ }
+ },
+ {
+ label: '娴佺▼瀹炰緥ID',
+ key: 'flowInstanceId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ祦绋嬪疄渚婭D'
+ }
+ },
+ {
+ label: '姝ラ瀹炰緥ID',
+ key: 'stepInstanceId',
+ type: 'inputNumber',
+ props: {
+ clearable: true,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ楠ゅ疄渚婭D'
+ }
+ },
+ {
+ label: '鏃ュ織绫诲瀷',
+ key: 'logType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ棩蹇楃被鍨�'
+ }
+ },
+ {
+ label: '鏃ュ織绾у埆',
+ key: 'logLevel',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ棩蹇楃骇鍒�'
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchFlowStepLogPage,
+ apiParams: buildFlowStepLogPageQueryParams(searchForm.value),
+ paginationKey: getFlowStepLogPaginationKey(),
+ columnsFactory: () => createFlowStepLogTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeFlowStepLogRow(item)) : []
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetFlowStepLogMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchFlowStepLogPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'flow-step-log.xlsx',
+ requestExport: (payload) =>
+ fetchExportFlowStepLogReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildFlowStepLogPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetFlowStepLogDetail(id)
+ detailData.value = normalizeFlowStepLogRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildFlowStepLogSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createFlowStepLogSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/flow-step-log/modules/flow-step-log-detail-drawer.vue b/rsf-design/src/views/system/flow-step-log/modules/flow-step-log-detail-drawer.vue
new file mode 100644
index 0000000..5890ebb
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-log/modules/flow-step-log-detail-drawer.vue
@@ -0,0 +1,39 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="娴佺▼姝ラ鏃ュ織璇︽儏"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="娴佺▼瀹炰緥ID">{{ detail.flowInstanceId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ラ瀹炰緥ID">{{ detail.stepInstanceId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏃ュ織绫诲瀷">{{ detail.logType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏃ュ織绾у埆">{{ detail.logLevel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏃ュ織鍐呭" :span="2">{{ detail.logContent || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璇锋眰鏁版嵁" :span="2">{{ detail.requestData || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍝嶅簲鏁版嵁" :span="2">{{ detail.responseData || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'FlowStepLogDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/flow-step-template/flowStepTemplatePage.helpers.js b/rsf-design/src/views/system/flow-step-template/flowStepTemplatePage.helpers.js
new file mode 100644
index 0000000..1d7855c
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-template/flowStepTemplatePage.helpers.js
@@ -0,0 +1,165 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const FLOW_STEP_TEMPLATE_REPORT_TITLE = '娴佺▼姝ラ妯℃澘鎶ヨ〃'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function normalizeBoolText(value) {
+ const numericValue = Number(value)
+ if (numericValue === 1) return '鏄�'
+ if (numericValue === 0) return '鍚�'
+ return '--'
+}
+
+export function createFlowStepTemplateSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ flowId: '',
+ flowCode: '',
+ stepOrder: '',
+ stepCode: '',
+ stepName: '',
+ stepType: '',
+ actionType: '',
+ actionConfig: '',
+ inputMapping: '',
+ outputMapping: '',
+ conditionExpression: '',
+ skipOnFail: '',
+ retryEnabled: '',
+ retryConfig: '',
+ timeoutSeconds: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getFlowStepTemplatePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildFlowStepTemplateSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'flowCode',
+ 'stepCode',
+ 'stepName',
+ 'stepType',
+ 'actionType',
+ 'actionConfig',
+ 'inputMapping',
+ 'outputMapping',
+ 'conditionExpression',
+ 'retryConfig',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;['flowId', 'stepOrder', 'skipOnFail', 'retryEnabled', 'timeoutSeconds', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildFlowStepTemplatePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildFlowStepTemplateSearchParams(params)
+ }
+}
+
+export function normalizeFlowStepTemplateRow(record = {}) {
+ const statusMeta = STATUS_META[Number(record.status)] || STATUS_META[0]
+ return {
+ ...record,
+ id: record.id ?? '--',
+ flowId: record.flowId ?? '--',
+ flowCode: normalizeText(record.flowCode) || '--',
+ stepOrder: record.stepOrder ?? '--',
+ stepCode: normalizeText(record.stepCode) || '--',
+ stepName: normalizeText(record.stepName) || '--',
+ stepType: normalizeText(record.stepType) || '--',
+ actionType: normalizeText(record.actionType) || '--',
+ skipOnFailText: normalizeBoolText(record.skipOnFail),
+ retryEnabledText: normalizeBoolText(record.retryEnabled),
+ timeoutSeconds: record.timeoutSeconds ?? '--',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function getFlowStepTemplateReportColumns() {
+ return [
+ { prop: 'flowId', label: '娴佺▼ID' },
+ { prop: 'flowCode', label: '娴佺▼缂栫爜' },
+ { prop: 'stepOrder', label: '姝ラ椤哄簭' },
+ { prop: 'stepCode', label: '姝ラ缂栫爜' },
+ { prop: 'stepName', label: '姝ラ鍚嶇О' },
+ { prop: 'stepType', label: '姝ラ绫诲瀷' },
+ { prop: 'actionType', label: '鍔ㄤ綔绫诲瀷' },
+ { prop: 'skipOnFailText', label: '璺宠繃澶辫触' },
+ { prop: 'retryEnabledText', label: '鍚敤閲嶈瘯' },
+ { prop: 'timeoutSeconds', label: '瓒呮椂(绉�)' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildFlowStepTemplatePrintRows(records = []) {
+ return (Array.isArray(records) ? records : []).map((record) => {
+ const row = normalizeFlowStepTemplateRow(record)
+ return {
+ flowId: row.flowId,
+ flowCode: row.flowCode,
+ stepOrder: row.stepOrder,
+ stepCode: row.stepCode,
+ stepName: row.stepName,
+ stepType: row.stepType,
+ actionType: row.actionType,
+ skipOnFailText: row.skipOnFailText,
+ retryEnabledText: row.retryEnabledText,
+ timeoutSeconds: row.timeoutSeconds,
+ statusText: row.statusText,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/flow-step-template/flowStepTemplateTable.columns.js b/rsf-design/src/views/system/flow-step-template/flowStepTemplateTable.columns.js
new file mode 100644
index 0000000..0af47e3
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-template/flowStepTemplateTable.columns.js
@@ -0,0 +1,48 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createFlowStepTemplateTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'flowId', label: '娴佺▼ID', width: 90, align: 'center' },
+ { prop: 'flowCode', label: '娴佺▼缂栫爜', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'stepOrder', label: '姝ラ椤哄簭', width: 100, align: 'center' },
+ { prop: 'stepCode', label: '姝ラ缂栫爜', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'stepName', label: '姝ラ鍚嶇О', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'stepType', label: '姝ラ绫诲瀷', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'actionType', label: '鍔ㄤ綔绫诲瀷', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'skipOnFailText', label: '璺宠繃澶辫触', width: 100, align: 'center' },
+ { prop: 'retryEnabledText', label: '鍚敤閲嶈瘯', width: 100, align: 'center' },
+ { prop: 'timeoutSeconds', label: '瓒呮椂(绉�)', width: 100, align: 'right' },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.statusType || 'info',
+ effect: 'light'
+ },
+ () => row?.statusText || '--'
+ )
+ },
+ { prop: 'memo', label: '澶囨敞', minWidth: 180, showOverflowTooltip: true },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/flow-step-template/index.vue b/rsf-design/src/views/system/flow-step-template/index.vue
new file mode 100644
index 0000000..a2e6ede
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-template/index.vue
@@ -0,0 +1,185 @@
+<template>
+ <div class="flow-step-template-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <FlowStepTemplateDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import {
+ fetchExportFlowStepTemplateReport,
+ fetchFlowStepTemplatePage,
+ fetchGetFlowStepTemplateDetail,
+ fetchGetFlowStepTemplateMany
+ } from '@/api/flow-step-template'
+ import {
+ buildFlowStepTemplatePageQueryParams,
+ buildFlowStepTemplatePrintRows,
+ buildFlowStepTemplateSearchParams,
+ createFlowStepTemplateSearchState,
+ FLOW_STEP_TEMPLATE_REPORT_TITLE,
+ getFlowStepTemplatePaginationKey,
+ getFlowStepTemplateReportColumns,
+ normalizeFlowStepTemplateRow
+ } from './flowStepTemplatePage.helpers'
+ import { createFlowStepTemplateTableColumns } from './flowStepTemplateTable.columns'
+ import FlowStepTemplateDetailDrawer from './modules/flow-step-template-detail-drawer.vue'
+
+ defineOptions({ name: 'FlowStepTemplate' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createFlowStepTemplateSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = FLOW_STEP_TEMPLATE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildFlowStepTemplateSearchParams(searchForm.value))
+ const reportColumns = getFlowStepTemplateReportColumns()
+
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋嬬紪鐮�/姝ラ缂栫爜' } },
+ { label: '娴佺▼ID', key: 'flowId', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋婭D' } },
+ { label: '娴佺▼缂栫爜', key: 'flowCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋嬬紪鐮�' } },
+ { label: '姝ラ缂栫爜', key: 'stepCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ楠ょ紪鐮�' } },
+ { label: '姝ラ鍚嶇О', key: 'stepName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ楠ゅ悕绉�' } },
+ { label: '姝ラ绫诲瀷', key: 'stepType', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ楠ょ被鍨�' } },
+ { label: '寮�濮嬫棩鏈�', key: 'timeStart', type: 'date', props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' } },
+ { label: '缁撴潫鏃ユ湡', key: 'timeEnd', type: 'date', props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' } }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchFlowStepTemplatePage,
+ apiParams: buildFlowStepTemplatePageQueryParams(searchForm.value),
+ paginationKey: getFlowStepTemplatePaginationKey(),
+ columnsFactory: () => createFlowStepTemplateTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeFlowStepTemplateRow(item)) : []
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetFlowStepTemplateMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchFlowStepTemplatePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'flow-step-template.xlsx',
+ requestExport: (payload) =>
+ fetchExportFlowStepTemplateReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildFlowStepTemplatePrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetFlowStepTemplateDetail(id)
+ detailData.value = normalizeFlowStepTemplateRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildFlowStepTemplateSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createFlowStepTemplateSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/flow-step-template/modules/flow-step-template-detail-drawer.vue b/rsf-design/src/views/system/flow-step-template/modules/flow-step-template-detail-drawer.vue
new file mode 100644
index 0000000..b0357da
--- /dev/null
+++ b/rsf-design/src/views/system/flow-step-template/modules/flow-step-template-detail-drawer.vue
@@ -0,0 +1,33 @@
+<template>
+ <ElDrawer :model-value="visible" title="娴佺▼姝ラ妯℃澘璇︽儏" size="620px" @close="emit('update:visible', false)">
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="娴佺▼ID">{{ detail.flowId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="娴佺▼缂栫爜">{{ detail.flowCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ラ椤哄簭">{{ detail.stepOrder || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ラ缂栫爜">{{ detail.stepCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ラ鍚嶇О">{{ detail.stepName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="姝ラ绫诲瀷">{{ detail.stepType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍔ㄤ綔绫诲瀷">{{ detail.actionType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璺宠繃澶辫触">{{ detail.skipOnFailText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚敤閲嶈瘯">{{ detail.retryEnabledText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瓒呮椂(绉�)">{{ detail.timeoutSeconds || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineProps({
+ visible: {
+ type: Boolean,
+ default: false
+ },
+ detail: {
+ type: Object,
+ default: () => ({})
+ }
+ })
+
+ const emit = defineEmits(['update:visible'])
+</script>
diff --git a/rsf-design/src/views/system/host/hostPage.helpers.js b/rsf-design/src/views/system/host/hostPage.helpers.js
new file mode 100644
index 0000000..e5a62c3
--- /dev/null
+++ b/rsf-design/src/views/system/host/hostPage.helpers.js
@@ -0,0 +1,89 @@
+const STATUS_OPTIONS = [
+ { label: '姝e父', value: 1 },
+ { label: '绂佺敤', value: 0 }
+]
+
+export function createHostSearchState() {
+ return {
+ condition: '',
+ name: '',
+ status: ''
+ }
+}
+
+export function createHostFormState() {
+ return {
+ id: null,
+ name: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getHostPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getHostStatusOptions() {
+ return STATUS_OPTIONS
+}
+
+export function getHostStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '姝e父', type: 'success', bool: true }
+ : { text: '绂佺敤', type: 'danger', bool: false }
+}
+
+export function buildHostSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ name: String(params.name || '').trim(),
+ ...(params.status !== '' && params.status !== null && params.status !== undefined
+ ? { status: Number(params.status) }
+ : {})
+ }
+}
+
+export function buildHostPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildHostSearchParams(params)
+ }
+}
+
+export function buildHostDialogModel(record = {}) {
+ return {
+ ...createHostFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ name: record.name || '',
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildHostSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ name: String(formData.name || '').trim(),
+ status: Number(formData.status ?? 1),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function normalizeHostListRow(record = {}) {
+ const statusMeta = getHostStatusMeta(record.status)
+ return {
+ ...record,
+ name: record.name || '',
+ memo: record.memo || '',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
diff --git a/rsf-design/src/views/system/host/hostTable.columns.js b/rsf-design/src/views/system/host/hostTable.columns.js
new file mode 100644
index 0000000..72b1f8d
--- /dev/null
+++ b/rsf-design/src/views/system/host/hostTable.columns.js
@@ -0,0 +1,68 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createHostTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'name',
+ label: '鏈烘瀯鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.createTimeText || '-'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/host/index.vue b/rsf-design/src/views/system/host/index.vue
new file mode 100644
index 0000000..87b8f7a
--- /dev/null
+++ b/rsf-design/src/views/system/host/index.vue
@@ -0,0 +1,210 @@
+<template>
+ <div class="host-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鏈烘瀯</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <HostDialog v-model:visible="dialogVisible" :host-data="currentHostData" @submit="handleDialogSubmit" />
+
+ <HostDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import {
+ fetchDeleteHost,
+ fetchGetHostDetail,
+ fetchHostPage,
+ fetchSaveHost,
+ fetchUpdateHost
+ } from '@/api/system-manage'
+ import HostDetailDrawer from './modules/host-detail-drawer.vue'
+ import HostDialog from './modules/host-dialog.vue'
+ import { createHostTableColumns } from './hostTable.columns'
+ import {
+ buildHostDialogModel,
+ buildHostPageQueryParams,
+ buildHostSavePayload,
+ buildHostSearchParams,
+ createHostSearchState,
+ getHostPaginationKey,
+ getHostStatusOptions,
+ normalizeHostListRow
+ } from './hostPage.helpers'
+
+ defineOptions({ name: 'Host' })
+
+ const { hasAuth } = useAuth()
+ const searchForm = ref(createHostSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ満鏋勫悕绉�'
+ }
+ },
+ {
+ label: '鏈烘瀯鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ満鏋勫悕绉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getHostStatusOptions()
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeHostListRow(await fetchGetHostDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鏈烘瀯璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentHostData.value = buildHostDialogModel(await fetchGetHostDetail(row.id))
+ dialogVisible.value = true
+ dialogType.value = 'edit'
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇鏈烘瀯璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchHostPage,
+ apiParams: buildHostPageQueryParams(searchForm.value),
+ paginationKey: getHostPaginationKey(),
+ columnsFactory: () =>
+ createHostTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeHostListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentHostData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildHostDialogModel(),
+ buildEditModel: (record) => buildHostDialogModel(record),
+ buildSavePayload: (formData) => buildHostSavePayload(formData),
+ saveRequest: fetchSaveHost,
+ updateRequest: fetchUpdateHost,
+ deleteRequest: fetchDeleteHost,
+ entityName: '鏈烘瀯',
+ resolveRecordLabel: (record) => record?.name || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ function handleSearch(params) {
+ replaceSearchParams(buildHostSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createHostSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/host/modules/host-detail-drawer.vue b/rsf-design/src/views/system/host/modules/host-detail-drawer.vue
new file mode 100644
index 0000000..630db90
--- /dev/null
+++ b/rsf-design/src/views/system/host/modules/host-detail-drawer.vue
@@ -0,0 +1,37 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鏈烘瀯璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="8">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="鏈烘瀯鍚嶇О">{{ displayData.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeHostListRow } from '../hostPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeHostListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/host/modules/host-dialog.vue b/rsf-design/src/views/system/host/modules/host-dialog.vue
new file mode 100644
index 0000000..60e423d
--- /dev/null
+++ b/rsf-design/src/views/system/host/modules/host-dialog.vue
@@ -0,0 +1,130 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="720px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import { buildHostDialogModel, createHostFormState, getHostStatusOptions } from '../hostPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ hostData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createHostFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫鏈烘瀯' : '鏂板鏈烘瀯'))
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ユ満鏋勫悕绉�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '鏈烘瀯鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ満鏋勫悕绉�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: getHostStatusOptions(),
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createHostFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildHostDialogModel(props.hostData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.hostData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/menu/index.vue b/rsf-design/src/views/system/menu/index.vue
index 2bb409c..e53a4dd 100644
--- a/rsf-design/src/views/system/menu/index.vue
+++ b/rsf-design/src/views/system/menu/index.vue
@@ -51,8 +51,7 @@
import MenuDialog from './modules/menu-dialog.vue'
import { formatMenuTitle } from '@/utils/router'
- import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue'
- import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
import { useTableColumns } from '@/hooks/core/useTableColumns'
import {
fetchDeleteMenu,
@@ -60,7 +59,16 @@
fetchSaveMenu,
fetchUpdateMenu
} from '@/api/system-manage'
- import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { createMenuTableColumns } from './menuTable.columns'
+ import {
+ buildMenuSubmitPayload,
+ buildMenuTreeOptions,
+ createMenuSearchState,
+ expandMenuAuthChildren,
+ filterMenuTree,
+ getMenuDisplayTitle
+ } from './menuPage.helpers'
defineOptions({ name: 'Menus' })
@@ -74,10 +82,7 @@
const tableData = ref([])
const menuTreeOptions = ref([])
- const initialSearchState = {
- name: '',
- route: ''
- }
+ const initialSearchState = createMenuSearchState()
const formFilters = reactive({ ...initialSearchState })
const appliedFilters = reactive({ ...initialSearchState })
@@ -97,39 +102,19 @@
}
])
- const normalizeNumber = (value, fallback = 0) => {
- if (value === '' || value === null || value === undefined) {
- return fallback
- }
- const normalized = Number(value)
- return Number.isNaN(normalized) ? fallback : normalized
- }
-
- const normalizeMenuTreeOptions = (nodes = []) => {
- if (!Array.isArray(nodes)) {
- return []
- }
-
- return nodes
- .map((node) => ({
- label: formatMenuTitle(node.meta?.title || node.name || ''),
- value: normalizeNumber(node.id, 0),
- children: normalizeMenuTreeOptions(node.children)
- }))
- }
-
const loadMenuResources = async () => {
loading.value = true
try {
- const list = await fetchGetMenuList({})
+ const list = await guardRequestWithMessage(fetchGetMenuList({}), null, {
+ timeoutMessage: '鑿滃崟鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ if (list === null) {
+ tableData.value = []
+ menuTreeOptions.value = []
+ return
+ }
tableData.value = Array.isArray(list) ? list : []
- menuTreeOptions.value = [
- {
- label: '椤剁骇鑿滃崟',
- value: 0,
- children: normalizeMenuTreeOptions(tableData.value)
- }
- ]
+ menuTreeOptions.value = buildMenuTreeOptions(tableData.value, formatMenuTitle)
} catch (error) {
ElMessage.error(error?.message || '鑾峰彇鑿滃崟澶辫触')
} finally {
@@ -141,237 +126,35 @@
loadMenuResources()
})
- const hasNestedMenus = (row) => Array.isArray(row.children) && row.children.some((child) => !child.meta?.isAuthButton)
-
- const getMenuTypeTag = (row) => {
- if (row.meta?.isAuthButton || Number(row.type) === 1) return 'danger'
- if (hasNestedMenus(row)) return 'info'
- return 'primary'
- }
-
- const getMenuTypeText = (row) => {
- if (row.meta?.isAuthButton || Number(row.type) === 1) return '鎸夐挳'
- if (hasNestedMenus(row)) return '鐩綍'
- return '鑿滃崟'
- }
-
- const getStatusMeta = (status) => {
- return normalizeNumber(status, 1) === 1
- ? { text: '鍚敤', type: 'success' }
- : { text: '绂佺敤', type: 'danger' }
- }
-
- const getMenuDisplayTitle = (row) => {
- const titleKey = row.meta?.title || row.name || ''
- const normalizedTitleKey =
- titleKey && !String(titleKey).includes('.') ? `menu.${titleKey}` : titleKey
-
- return formatMenuTitle(normalizedTitleKey)
- }
-
- const getMenuDisplayIcon = (row) => row.meta?.icon || row.icon || ''
-
- const { columnChecks, columns } = useTableColumns(() => [
- {
- prop: 'meta.title',
- label: '鑿滃崟鍚嶇О',
- minWidth: 180,
- formatter: (row) => getMenuDisplayTitle(row)
- },
- {
- prop: 'meta.icon',
- label: '鍥炬爣棰勮',
- width: 96,
- align: 'center',
- formatter: (row) => {
- const icon = getMenuDisplayIcon(row)
-
- if (!icon) return h('span', { class: 'text-g-400' }, '-')
-
- return h(
- 'div',
- {
- class:
- 'mx-auto flex h-8 w-8 items-center justify-center rounded-md border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)]'
- },
- [h(ArtSvgIcon, { icon, class: 'text-base text-g-700' })]
- )
- }
- },
- {
- prop: 'type',
- label: '鑿滃崟绫诲瀷',
- width: 110,
- formatter: (row) =>
- h(ElTag, { type: getMenuTypeTag(row), effect: 'light' }, () => getMenuTypeText(row))
- },
- {
- prop: 'route',
- label: '璺敱',
- minWidth: 180,
- formatter: (row) => {
- if (row.meta?.isAuthButton) return ''
- return row.route || ''
- }
- },
- {
- prop: 'authority',
- label: '鏉冮檺鏍囪瘑',
- minWidth: 180,
- formatter: (row) => {
- if (row.meta?.isAuthButton) {
- return row.authority || row.meta?.authMark || ''
- }
- if (!row.meta?.authList?.length) return row.authority || ''
- return `${row.meta.authList.length} 涓潈闄愭爣璇哷
- }
- },
- {
- prop: 'sort',
- label: '鎺掑簭',
- width: 90
- },
- {
- prop: 'status',
- label: '鐘舵��',
- width: 100,
- formatter: (row) => {
- const statusMeta = getStatusMeta(row.status)
- return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
- }
- },
- {
- prop: 'memo',
- label: '澶囨敞',
- minWidth: 180,
- showOverflowTooltip: true,
- formatter: (row) => row.memo || '-'
- },
- {
- prop: 'operation',
- label: '鎿嶄綔',
- width: 180,
- align: 'right',
- formatter: (row) => {
- const buttonStyle = { class: 'flex justify-end' }
- if (row.meta?.isAuthButton) {
- return h('div', buttonStyle, [
- h(ArtButtonTable, {
- type: 'edit',
- onClick: () => handleEditAuth(row)
- }),
- h(ArtButtonTable, {
- type: 'delete',
- onClick: () => handleDeleteAuth(row)
- })
- ])
- }
- return h('div', buttonStyle, [
- h(ArtButtonTable, {
- type: 'add',
- onClick: () => handleAddAuth(row),
- title: '鏂板鏉冮檺'
- }),
- h(ArtButtonTable, {
- type: 'edit',
- onClick: () => handleEditMenu(row)
- }),
- h(ArtButtonTable, {
- type: 'delete',
- onClick: () => handleDeleteMenu(row)
- })
- ])
- }
- },
- ])
-
- const deepClone = (obj) => {
- if (obj === null || typeof obj !== 'object') return obj
- if (obj instanceof Date) return new Date(obj)
- if (Array.isArray(obj)) return obj.map((item) => deepClone(item))
- const cloned = {}
- for (const key in obj) {
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
- cloned[key] = deepClone(obj[key])
- }
- }
- return cloned
- }
-
- const convertAuthListToChildren = (items) => {
- return items.map((item) => {
- const clonedItem = deepClone(item)
- if (clonedItem.children?.length) {
- clonedItem.children = convertAuthListToChildren(clonedItem.children)
- }
- if (item.meta?.authList?.length) {
- const authChildren = item.meta.authList.map((auth) => ({
- ...deepClone(auth),
- route: auth.route || '',
- component: auth.component || '',
- meta: {
- title: auth.title,
- authMark: auth.authMark,
- isAuthButton: true,
- parentPath: item.path,
- icon: auth.icon,
- sort: auth.sort,
- isEnable: normalizeNumber(auth.status, 1) === 1
- }
- }))
- clonedItem.children = clonedItem.children?.length
- ? [...clonedItem.children, ...authChildren]
- : authChildren
- }
- return clonedItem
+ const { columnChecks, columns } = useTableColumns(() =>
+ createMenuTableColumns({
+ titleFormatter: formatMenuTitle,
+ handleAddAuth,
+ handleEditAuth,
+ handleDeleteAuth,
+ handleEditMenu,
+ handleDeleteMenu
})
- }
-
- const searchMenu = (items) => {
- const results = []
- for (const item of items) {
- const searchName = appliedFilters.name?.toLowerCase().trim() || ''
- const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
- const menuTitle = getMenuDisplayTitle(item).toLowerCase()
- const menuRoute = String(item.route || item.path || item.authority || '').toLowerCase()
- const nameMatch = !searchName || menuTitle.includes(searchName)
- const routeMatch = !searchRoute || menuRoute.includes(searchRoute)
-
- if (item.children?.length) {
- const matchedChildren = searchMenu(item.children)
- if (matchedChildren.length > 0) {
- const clonedItem = deepClone(item)
- clonedItem.children = matchedChildren
- results.push(clonedItem)
- continue
- }
- }
-
- if (nameMatch && routeMatch) {
- results.push(deepClone(item))
- }
- }
- return results
- }
+ )
const filteredTableData = computed(() => {
- const searchedData = searchMenu(tableData.value)
- return convertAuthListToChildren(searchedData)
+ const searchedData = filterMenuTree(tableData.value, appliedFilters, formatMenuTitle)
+ return expandMenuAuthChildren(searchedData)
})
- const closeDialog = () => {
+ function closeDialog() {
dialogVisible.value = false
editData.value = null
}
- const handleAddMenu = () => {
+ function handleAddMenu() {
dialogType.value = 'menu'
editData.value = null
lockMenuType.value = true
dialogVisible.value = true
}
- const handleAddAuth = (row) => {
+ function handleAddAuth(row) {
dialogType.value = 'button'
editData.value = {
parentId: row.id,
@@ -383,37 +166,21 @@
dialogVisible.value = true
}
- const handleEditMenu = (row) => {
+ function handleEditMenu(row) {
dialogType.value = 'menu'
editData.value = row
lockMenuType.value = true
dialogVisible.value = true
}
- const handleEditAuth = (row) => {
+ function handleEditAuth(row) {
dialogType.value = 'button'
editData.value = row
lockMenuType.value = true
dialogVisible.value = true
}
- const buildMenuSubmitPayload = (formData) => {
- return {
- ...(formData.id ? { id: normalizeNumber(formData.id, 0) } : {}),
- parentId: normalizeNumber(formData.parentId, 0),
- name: String(formData.name || '').trim(),
- route: String(formData.route || '').trim(),
- component: String(formData.component || '').trim(),
- authority: String(formData.authority || '').trim(),
- icon: String(formData.icon || '').trim(),
- sort: normalizeNumber(formData.sort, 0),
- status: normalizeNumber(formData.status, 1),
- memo: String(formData.memo || '').trim(),
- type: formData.menuType === 'button' ? 1 : 0
- }
- }
-
- const handleSubmit = async (formData) => {
+ async function handleSubmit(formData) {
const payload = buildMenuSubmitPayload(formData)
if (payload.id && payload.id === payload.parentId) {
ElMessage.error('涓婄骇鑿滃崟涓嶈兘閫夋嫨褰撳墠鑿滃崟')
@@ -435,10 +202,10 @@
}
}
- const handleDeleteMenu = async (row) => {
+ async function handleDeleteMenu(row) {
try {
await ElMessageBox.confirm(
- `纭畾瑕佸垹闄よ彍鍗曘��${formatMenuTitle(row.meta?.title || row.name || '')}銆嶅悧锛熷垹闄ゅ悗鏃犳硶鎭㈠`,
+ `纭畾瑕佸垹闄よ彍鍗曘��${getMenuDisplayTitle(row, formatMenuTitle)}銆嶅悧锛熷垹闄ゅ悗鏃犳硶鎭㈠`,
'鍒犻櫎纭',
{
confirmButtonText: '纭畾',
@@ -456,7 +223,7 @@
}
}
- const handleDeleteAuth = async (row) => {
+ async function handleDeleteAuth(row) {
try {
await ElMessageBox.confirm(`纭畾瑕佸垹闄ゆ潈闄愩��${row.name || row.authority || row.id}銆嶅悧锛熷垹闄ゅ悗鏃犳硶鎭㈠`, '鍒犻櫎纭', {
confirmButtonText: '纭畾',
@@ -473,21 +240,21 @@
}
}
- const handleReset = () => {
+ function handleReset() {
Object.assign(formFilters, { ...initialSearchState })
Object.assign(appliedFilters, { ...initialSearchState })
loadMenuResources()
}
- const handleSearch = () => {
+ function handleSearch() {
Object.assign(appliedFilters, { ...formFilters })
}
- const handleRefresh = () => {
+ function handleRefresh() {
loadMenuResources()
}
- const toggleExpand = () => {
+ function toggleExpand() {
isExpanded.value = !isExpanded.value
nextTick(() => {
if (tableRef.value?.elTableRef && filteredTableData.value) {
diff --git a/rsf-design/src/views/system/menu/menuPage.helpers.js b/rsf-design/src/views/system/menu/menuPage.helpers.js
new file mode 100644
index 0000000..20e605c
--- /dev/null
+++ b/rsf-design/src/views/system/menu/menuPage.helpers.js
@@ -0,0 +1,164 @@
+export function createMenuSearchState() {
+ return {
+ name: '',
+ route: ''
+ }
+}
+
+export function normalizeMenuNumber(value, fallback = 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const normalized = Number(value)
+ return Number.isNaN(normalized) ? fallback : normalized
+}
+
+export function normalizeMenuTitleKey(row = {}) {
+ const titleKey = row.meta?.title || row.name || ''
+ return titleKey && !String(titleKey).includes('.') ? `menu.${titleKey}` : titleKey
+}
+
+export function defaultMenuTitleFormatter(title = '') {
+ if (!title) {
+ return ''
+ }
+ return String(title).split('.').pop() || String(title)
+}
+
+export function getMenuDisplayTitle(row = {}, titleFormatter = defaultMenuTitleFormatter) {
+ return titleFormatter(normalizeMenuTitleKey(row))
+}
+
+export function getMenuDisplayIcon(row = {}) {
+ return row.meta?.icon || row.icon || ''
+}
+
+export function hasNestedMenus(row = {}) {
+ return Array.isArray(row.children) && row.children.some((child) => !child.meta?.isAuthButton)
+}
+
+export function getMenuTypeTag(row = {}) {
+ if (row.meta?.isAuthButton || Number(row.type) === 1) return 'danger'
+ if (hasNestedMenus(row)) return 'info'
+ return 'primary'
+}
+
+export function getMenuTypeText(row = {}) {
+ if (row.meta?.isAuthButton || Number(row.type) === 1) return '鎸夐挳'
+ if (hasNestedMenus(row)) return '鐩綍'
+ return '鑿滃崟'
+}
+
+export function getMenuStatusMeta(status) {
+ return normalizeMenuNumber(status, 1) === 1
+ ? { text: '鍚敤', type: 'success' }
+ : { text: '绂佺敤', type: 'danger' }
+}
+
+export function normalizeMenuTreeOptions(nodes = [], titleFormatter = defaultMenuTitleFormatter) {
+ if (!Array.isArray(nodes)) {
+ return []
+ }
+
+ return nodes.map((node) => ({
+ label: getMenuDisplayTitle(node, titleFormatter),
+ value: normalizeMenuNumber(node.id, 0),
+ children: normalizeMenuTreeOptions(node.children, titleFormatter)
+ }))
+}
+
+export function buildMenuTreeOptions(tree = [], titleFormatter = defaultMenuTitleFormatter) {
+ return [
+ {
+ label: '椤剁骇鑿滃崟',
+ value: 0,
+ children: normalizeMenuTreeOptions(tree, titleFormatter)
+ }
+ ]
+}
+
+export function buildMenuSubmitPayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: normalizeMenuNumber(formData.id, 0) } : {}),
+ parentId: normalizeMenuNumber(formData.parentId, 0),
+ name: String(formData.name || '').trim(),
+ route: String(formData.route || '').trim(),
+ component: String(formData.component || '').trim(),
+ authority: String(formData.authority || '').trim(),
+ icon: String(formData.icon || '').trim(),
+ sort: normalizeMenuNumber(formData.sort, 0),
+ status: normalizeMenuNumber(formData.status, 1),
+ memo: String(formData.memo || '').trim(),
+ type: formData.menuType === 'button' ? 1 : 0
+ }
+}
+
+export function cloneMenuTree(source) {
+ if (source === null || typeof source !== 'object') return source
+ if (source instanceof Date) return new Date(source)
+ if (Array.isArray(source)) return source.map((item) => cloneMenuTree(item))
+ const cloned = {}
+ for (const key in source) {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ cloned[key] = cloneMenuTree(source[key])
+ }
+ }
+ return cloned
+}
+
+export function expandMenuAuthChildren(items = []) {
+ return items.map((item) => {
+ const clonedItem = cloneMenuTree(item)
+ if (clonedItem.children?.length) {
+ clonedItem.children = expandMenuAuthChildren(clonedItem.children)
+ }
+ if (item.meta?.authList?.length) {
+ const authChildren = item.meta.authList.map((auth) => ({
+ ...cloneMenuTree(auth),
+ route: auth.route || '',
+ component: auth.component || '',
+ meta: {
+ title: auth.title,
+ authMark: auth.authMark,
+ isAuthButton: true,
+ parentPath: item.path,
+ icon: auth.icon,
+ sort: auth.sort,
+ isEnable: normalizeMenuNumber(auth.status, 1) === 1
+ }
+ }))
+ clonedItem.children = clonedItem.children?.length
+ ? [...clonedItem.children, ...authChildren]
+ : authChildren
+ }
+ return clonedItem
+ })
+}
+
+export function filterMenuTree(items = [], filters = {}, titleFormatter = defaultMenuTitleFormatter) {
+ const results = []
+ const searchName = String(filters.name || '').toLowerCase().trim()
+ const searchRoute = String(filters.route || '').toLowerCase().trim()
+
+ for (const item of items) {
+ const menuTitle = getMenuDisplayTitle(item, titleFormatter).toLowerCase()
+ const menuRoute = String(item.route || item.path || item.authority || '').toLowerCase()
+ const nameMatch = !searchName || menuTitle.includes(searchName)
+ const routeMatch = !searchRoute || menuRoute.includes(searchRoute)
+
+ if (item.children?.length) {
+ const matchedChildren = filterMenuTree(item.children, filters, titleFormatter)
+ if (matchedChildren.length > 0) {
+ const clonedItem = cloneMenuTree(item)
+ clonedItem.children = matchedChildren
+ results.push(clonedItem)
+ continue
+ }
+ }
+
+ if (nameMatch && routeMatch) {
+ results.push(cloneMenuTree(item))
+ }
+ }
+ return results
+}
diff --git a/rsf-design/src/views/system/menu/menuTable.columns.js b/rsf-design/src/views/system/menu/menuTable.columns.js
new file mode 100644
index 0000000..a27b767
--- /dev/null
+++ b/rsf-design/src/views/system/menu/menuTable.columns.js
@@ -0,0 +1,134 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+import {
+ getMenuDisplayIcon,
+ getMenuDisplayTitle,
+ getMenuStatusMeta,
+ getMenuTypeTag,
+ getMenuTypeText
+} from './menuPage.helpers'
+
+export function createMenuTableColumns({
+ titleFormatter,
+ handleAddAuth,
+ handleEditAuth,
+ handleDeleteAuth,
+ handleEditMenu,
+ handleDeleteMenu
+}) {
+ return [
+ {
+ prop: 'meta.title',
+ label: '鑿滃崟鍚嶇О',
+ minWidth: 180,
+ formatter: (row) => getMenuDisplayTitle(row, titleFormatter)
+ },
+ {
+ prop: 'meta.icon',
+ label: '鍥炬爣棰勮',
+ width: 96,
+ align: 'center',
+ formatter: (row) => {
+ const icon = getMenuDisplayIcon(row)
+
+ if (!icon) return h('span', { class: 'text-g-400' }, '-')
+
+ return h(
+ 'div',
+ {
+ class:
+ 'mx-auto flex h-8 w-8 items-center justify-center rounded-md border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)]'
+ },
+ [h(ArtSvgIcon, { icon, class: 'text-base text-g-700' })]
+ )
+ }
+ },
+ {
+ prop: 'type',
+ label: '鑿滃崟绫诲瀷',
+ width: 110,
+ formatter: (row) =>
+ h(ElTag, { type: getMenuTypeTag(row), effect: 'light' }, () => getMenuTypeText(row))
+ },
+ {
+ prop: 'route',
+ label: '璺敱',
+ minWidth: 180,
+ formatter: (row) => {
+ if (row.meta?.isAuthButton) return ''
+ return row.route || ''
+ }
+ },
+ {
+ prop: 'authority',
+ label: '鏉冮檺鏍囪瘑',
+ minWidth: 180,
+ formatter: (row) => {
+ if (row.meta?.isAuthButton) {
+ return row.authority || row.meta?.authMark || ''
+ }
+ if (!row.meta?.authList?.length) return row.authority || ''
+ return `${row.meta.authList.length} 涓潈闄愭爣璇哷
+ }
+ },
+ {
+ prop: 'sort',
+ label: '鎺掑簭',
+ width: 90
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => {
+ const statusMeta = getMenuStatusMeta(row.status)
+ return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+ }
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 180,
+ align: 'right',
+ formatter: (row) => {
+ const buttonStyle = { class: 'flex justify-end' }
+ if (row.meta?.isAuthButton) {
+ return h('div', buttonStyle, [
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEditAuth(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDeleteAuth(row)
+ })
+ ])
+ }
+ return h('div', buttonStyle, [
+ h(ArtButtonTable, {
+ type: 'add',
+ onClick: () => handleAddAuth(row),
+ title: '鏂板鏉冮檺'
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEditMenu(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDeleteMenu(row)
+ })
+ ])
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/operation-record/index.vue b/rsf-design/src/views/system/operation-record/index.vue
new file mode 100644
index 0000000..b8005c6
--- /dev/null
+++ b/rsf-design/src/views/system/operation-record/index.vue
@@ -0,0 +1,281 @@
+<template>
+ <div class="operation-record-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <span v-auth="'list'" class="inline-flex">
+ <ListExportPrint
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </span>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <OperationRecordDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ fetchDeleteOperationRecord,
+ fetchExportOperationRecordReport,
+ fetchGetOperationRecordDetail,
+ fetchGetOperationRecordMany,
+ fetchOperationRecordPage
+ } from '@/api/system-manage'
+ import {
+ buildOperationRecordPageQueryParams,
+ buildOperationRecordPrintRows,
+ buildOperationRecordSearchParams,
+ createOperationRecordSearchState,
+ getOperationRecordPaginationKey,
+ getOperationRecordReportColumns,
+ mergeOperationRecordDetail,
+ normalizeOperationRecordRow,
+ OPERATION_RECORD_REPORT_TITLE
+ } from './operationRecordPage.helpers'
+ import { createOperationRecordTableColumns } from './operationRecordTable.columns'
+ import OperationRecordDetailDrawer from './modules/operation-record-detail-drawer.vue'
+
+ defineOptions({ name: 'OperationRecord' })
+
+ const userStore = useUserStore()
+ const { hasAuth } = useAuth()
+
+ const searchForm = ref(createOperationRecordSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = OPERATION_RECORD_REPORT_TITLE
+ const reportQueryParams = computed(() => buildOperationRecordSearchParams(searchForm.value))
+ const reportColumns = getOperationRecordReportColumns()
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悕绉扮┖闂�'
+ }
+ },
+ {
+ label: '鎺ュ彛鍦板潃',
+ key: 'url',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ帴鍙e湴鍧�'
+ }
+ },
+ {
+ label: '瀹㈡埛绔疘P',
+ key: 'clientIp',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ鎴风IP'
+ }
+ },
+ {
+ label: '缁撴灉',
+ key: 'result',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '鎴愬姛', value: 1 },
+ { label: '澶辫触', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadOperationDetail(row.id, row)
+ }
+
+ async function handleDelete(row) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄ゆ搷浣滄棩蹇椼��${row.namespace || row.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteOperationRecord(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshRemove()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchOperationRecordPage,
+ apiParams: buildOperationRecordPageQueryParams(searchForm.value),
+ paginationKey: getOperationRecordPaginationKey(),
+ columnsFactory: () =>
+ createOperationRecordTableColumns({
+ handleView: openDetail,
+ handleDelete: hasAuth('delete') ? handleDelete : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeOperationRecordRow(item))
+ }
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetOperationRecordMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchOperationRecordPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'operation-record.xlsx',
+ requestExport: (payload) =>
+ fetchExportOperationRecordReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildOperationRecordPrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadOperationDetail(id, fallback) {
+ detailLoading.value = true
+ try {
+ const detail = await fetchGetOperationRecordDetail(id)
+ detailData.value = mergeOperationRecordDetail(detail, fallback)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鎿嶄綔鏃ュ織璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildOperationRecordSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createOperationRecordSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/operation-record/modules/operation-record-detail-drawer.vue b/rsf-design/src/views/system/operation-record/modules/operation-record-detail-drawer.vue
new file mode 100644
index 0000000..98ab2c1
--- /dev/null
+++ b/rsf-design/src/views/system/operation-record/modules/operation-record-detail-drawer.vue
@@ -0,0 +1,53 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="鎿嶄綔鏃ュ織璇︽儏"
+ size="640px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="12">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="鍚嶇О绌洪棿">{{ displayData.namespace || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎺ュ彛鍦板潃">{{ displayData.url || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="骞冲彴瀵嗛挜">{{ displayData.appkey || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎿嶄綔鐢ㄦ埛">{{ displayData.userLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹㈡埛绔疘P">{{ displayData.clientIp || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁撴灉">
+ <ElTag :type="displayData.resultType" effect="light">{{ displayData.resultText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鑰楁椂(ms)">{{ displayData.spendTime ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎿嶄綔鏃堕棿">{{ displayData.timestampText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寮傚父淇℃伅">{{ displayData.err || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璇锋眰鍐呭">
+ <pre class="whitespace-pre-wrap break-all rounded-lg bg-[var(--art-main-bg-color)] p-3 text-xs leading-6 text-g-700">{{
+ displayData.request || '--'
+ }}</pre>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鍝嶅簲鍐呭">
+ <pre class="whitespace-pre-wrap break-all rounded-lg bg-[var(--art-main-bg-color)] p-3 text-xs leading-6 text-g-700">{{
+ displayData.response || '--'
+ }}</pre>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { mergeOperationRecordDetail } from '../operationRecordPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const displayData = computed(() => mergeOperationRecordDetail(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/operation-record/operationRecordPage.helpers.js b/rsf-design/src/views/system/operation-record/operationRecordPage.helpers.js
new file mode 100644
index 0000000..fb55e27
--- /dev/null
+++ b/rsf-design/src/views/system/operation-record/operationRecordPage.helpers.js
@@ -0,0 +1,123 @@
+const OPERATION_RESULT_META = {
+ 1: { text: '鎴愬姛', type: 'success' },
+ 0: { text: '澶辫触', type: 'danger' }
+}
+
+export const OPERATION_RECORD_REPORT_TITLE = '鎿嶄綔鏃ュ織鎶ヨ〃'
+
+export function createOperationRecordSearchState() {
+ return {
+ condition: '',
+ namespace: '',
+ url: '',
+ clientIp: '',
+ result: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function getOperationRecordPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildOperationRecordSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ namespace: String(params.namespace || '').trim(),
+ url: String(params.url || '').trim(),
+ clientIp: String(params.clientIp || '').trim(),
+ ...(params.result !== '' && params.result !== null && params.result !== undefined
+ ? { result: Number(params.result) }
+ : {}),
+ ...(params.timeStart ? { timeStart: params.timeStart } : {}),
+ ...(params.timeEnd ? { timeEnd: params.timeEnd } : {})
+ }
+}
+
+export function buildOperationRecordPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildOperationRecordSearchParams(params)
+ }
+}
+
+export function getOperationResultMeta(value) {
+ return OPERATION_RESULT_META[Number(value)] || { text: '-', type: 'info' }
+}
+
+export function formatOperationTimestamp(value) {
+ if (value === '' || value === null || value === undefined) {
+ return ''
+ }
+
+ const timestamp = Number(value)
+ const date = Number.isNaN(timestamp) ? new Date(value) : new Date(timestamp)
+ if (Number.isNaN(date.getTime())) {
+ return String(value)
+ }
+
+ const pad = (segment) => String(segment).padStart(2, '0')
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(
+ date.getMinutes()
+ )}:${pad(date.getSeconds())}`
+}
+
+export function normalizeOperationRecordRow(record = {}) {
+ const resultMeta = getOperationResultMeta(record.result)
+ return {
+ ...record,
+ namespace: record.namespace || '',
+ url: record.url || '',
+ appkey: record.appkey || '',
+ request: record.request || '',
+ response: record.response || '',
+ err: record.err || '',
+ memo: record.memo || '',
+ clientIp: record.clientIp || '',
+ spendTime: record.spendTime ?? 0,
+ userLabel: record['userId$'] || record.userLabel || (record.userId ? String(record.userId) : ''),
+ timestampText: formatOperationTimestamp(record.timestamp),
+ createTimeText: record['createTime$'] || record.createTime || '',
+ resultText: resultMeta.text,
+ resultType: resultMeta.type
+ }
+}
+
+export function mergeOperationRecordDetail(detail = {}, fallback = {}) {
+ return normalizeOperationRecordRow({
+ ...fallback,
+ ...detail
+ })
+}
+
+export function getOperationRecordReportColumns() {
+ return [
+ { prop: 'namespace', label: '鍚嶇О绌洪棿' },
+ { prop: 'url', label: '鎺ュ彛鍦板潃' },
+ { prop: 'userLabel', label: '鎿嶄綔鐢ㄦ埛' },
+ { prop: 'clientIp', label: '瀹㈡埛绔疘P' },
+ { prop: 'spendTimeText', label: '鑰楁椂(ms)' },
+ { prop: 'resultText', label: '缁撴灉' },
+ { prop: 'timestampText', label: '鎿嶄綔鏃堕棿' }
+ ]
+}
+
+export function buildOperationRecordPrintRows(records = []) {
+ return records.map((record) => {
+ const row = normalizeOperationRecordRow(record)
+ return {
+ namespace: row.namespace || '--',
+ url: row.url || '--',
+ userLabel: row.userLabel || '--',
+ clientIp: row.clientIp || '--',
+ spendTimeText: row.spendTime ?? '--',
+ resultText: row.resultText || '--',
+ timestampText: row.timestampText || '--'
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/operation-record/operationRecordTable.columns.js b/rsf-design/src/views/system/operation-record/operationRecordTable.columns.js
new file mode 100644
index 0000000..24f270f
--- /dev/null
+++ b/rsf-design/src/views/system/operation-record/operationRecordTable.columns.js
@@ -0,0 +1,76 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createOperationRecordTableColumns({ handleView, handleDelete }) {
+ return [
+ {
+ prop: 'namespace',
+ label: '鍚嶇О绌洪棿',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'url',
+ label: '鎺ュ彛鍦板潃',
+ minWidth: 240,
+ showOverflowTooltip: true,
+ formatter: (row) => row.url || '-'
+ },
+ {
+ prop: 'userLabel',
+ label: '鎿嶄綔鐢ㄦ埛',
+ minWidth: 140,
+ formatter: (row) => row.userLabel || '-'
+ },
+ {
+ prop: 'clientIp',
+ label: '瀹㈡埛绔疘P',
+ minWidth: 140,
+ formatter: (row) => row.clientIp || '-'
+ },
+ {
+ prop: 'spendTime',
+ label: '鑰楁椂(ms)',
+ width: 110,
+ formatter: (row) => row.spendTime ?? '-'
+ },
+ {
+ prop: 'result',
+ label: '缁撴灉',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.resultType, effect: 'light' }, () => row.resultText || '-')
+ },
+ {
+ prop: 'timestampText',
+ label: '鎿嶄綔鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.timestampText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 120 : 70,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
index 779b85f..c7c58ec 100644
--- a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
+++ b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
@@ -78,6 +78,7 @@
} from '../rolePage.helpers'
import { fetchGetRoleScopeList, fetchGetRoleScopeTree, fetchUpdateRoleScope } from '@/api/system-manage'
import { resolveBackendMenuTitle } from '@/utils/backend-menu-title'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
import { ElMessage } from 'element-plus'
const props = defineProps({
@@ -128,7 +129,21 @@
requests.unshift(fetchGetRoleScopeList(config.scopeType, props.roleData.id))
}
- const [checkedIds, treeData] = reloadSelection ? await Promise.all(requests) : [state.checkedKeys, await requests[0]]
+ const guardedResult = await guardRequestWithMessage(
+ reloadSelection ? Promise.all(requests) : Promise.resolve([state.checkedKeys, await requests[0]]),
+ null,
+ {
+ timeoutMessage: `${config.title}鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟`
+ }
+ )
+ if (!guardedResult) {
+ state.treeData = []
+ state.checkedKeys = []
+ state.halfCheckedKeys = []
+ state.loaded = true
+ return
+ }
+ const [checkedIds, treeData] = guardedResult
state.treeData = normalizeRoleScopeTreeData(config.scopeType, treeData)
state.checkedKeys = normalizeScopeKeys(checkedIds)
state.halfCheckedKeys = []
diff --git a/rsf-design/src/views/system/serial-rule-item/index.vue b/rsf-design/src/views/system/serial-rule-item/index.vue
new file mode 100644
index 0000000..08b7cc0
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule-item/index.vue
@@ -0,0 +1,430 @@
+<template>
+ <div class="serial-rule-item-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板缂栫爜瑙勫垯鏄庣粏</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="columns"
+ :preview-rows="previewRows"
+ :preview-meta="resolvedPreviewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <SerialRuleItemDialog
+ v-model:visible="dialogVisible"
+ :serial-rule-item-data="currentSerialRuleItemData"
+ :wk-type-options="wkTypeOptions"
+ @submit="handleDialogSubmit"
+ />
+
+ <SerialRuleItemDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import { fetchDictDataPage } from '@/api/system-manage'
+ import {
+ fetchDeleteSerialRuleItem,
+ fetchExportSerialRuleItemReport,
+ fetchGetSerialRuleItemDetail,
+ fetchGetSerialRuleItemMany,
+ fetchSaveSerialRuleItem,
+ fetchSerialRuleItemPage,
+ fetchUpdateSerialRuleItem
+ } from '@/api/serial-rule-item'
+ import SerialRuleItemDialog from './modules/serial-rule-item-dialog.vue'
+ import SerialRuleItemDetailDrawer from './modules/serial-rule-item-detail-drawer.vue'
+ import { createSerialRuleItemTableColumns } from './serialRuleItemTable.columns'
+ import {
+ buildSerialRuleItemDialogModel,
+ buildSerialRuleItemPageQueryParams,
+ buildSerialRuleItemPrintRows,
+ buildSerialRuleItemReportMeta,
+ buildSerialRuleItemSavePayload,
+ buildSerialRuleItemSearchParams,
+ createSerialRuleItemSearchState,
+ getSerialRuleItemPaginationKey,
+ getSerialRuleItemStatusOptions,
+ normalizeSerialRuleItemListRow,
+ SERIAL_RULE_ITEM_REPORT_STYLE,
+ SERIAL_RULE_ITEM_REPORT_TITLE
+ } from './serialRuleItemPage.helpers'
+
+ defineOptions({ name: 'SerialRuleItem' })
+
+ const { hasAuth } = useAuth()
+ const userStore = useUserStore()
+
+ const searchForm = ref(createSerialRuleItemSearchState())
+ const wkTypeOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const reportTitle = SERIAL_RULE_ITEM_REPORT_TITLE
+ const reportQueryParams = computed(() => buildSerialRuleItemSearchParams(searchForm.value))
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ鍒欎富琛ㄦ爣璇�/瀛楁鍊�'
+ }
+ },
+ {
+ label: '瑙勫垯涓昏〃鏍囪瘑',
+ key: 'ruleId',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ鍒欎富琛ㄦ爣璇�',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '瑙勫垯绫诲瀷',
+ key: 'wkType',
+ type: 'select',
+ props: {
+ clearable: true,
+ filterable: true,
+ options: wkTypeOptions.value
+ }
+ },
+ {
+ label: '瀛楁鍊�',
+ key: 'feildValue',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ瓧娈靛��'
+ }
+ },
+ {
+ label: '鎴彇闀垮害',
+ key: 'len',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ埅鍙栭暱搴�',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '璧峰浣嶇疆',
+ key: 'lenStr',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ捣濮嬩綅缃�',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '鎺掑簭椤哄簭',
+ key: 'sort',
+ type: 'number',
+ props: {
+ clearable: true,
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ帓搴忛『搴�',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getSerialRuleItemStatusOptions()
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨寮�濮嬫椂闂�'
+ }
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨缁撴潫鏃堕棿'
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetSerialRuleItemDetail(row.id), {}, {
+ timeoutMessage: '缂栫爜瑙勫垯鏄庣粏璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeSerialRuleItemListRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇缂栫爜瑙勫垯鏄庣粏璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ const detail = await guardRequestWithMessage(fetchGetSerialRuleItemDetail(row.id), {}, {
+ timeoutMessage: '缂栫爜瑙勫垯鏄庣粏璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ currentSerialRuleItemData.value = buildSerialRuleItemDialogModel(detail)
+ dialogVisible.value = true
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇缂栫爜瑙勫垯鏄庣粏璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchSerialRuleItemPage,
+ apiParams: buildSerialRuleItemPageQueryParams(searchForm.value),
+ paginationKey: getSerialRuleItemPaginationKey(),
+ columnsFactory: () =>
+ createSerialRuleItemTableColumns({
+ handleView: openDetail,
+ handleEdit: hasAuth('update') ? openEditDialog : null,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeSerialRuleItemListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ currentRecord: currentSerialRuleItemData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildSerialRuleItemDialogModel(),
+ buildEditModel: (record) => buildSerialRuleItemDialogModel(record),
+ buildSavePayload: (formData) => buildSerialRuleItemSavePayload(formData),
+ saveRequest: fetchSaveSerialRuleItem,
+ updateRequest: fetchUpdateSerialRuleItem,
+ deleteRequest: fetchDeleteSerialRuleItem,
+ entityName: '缂栫爜瑙勫垯鏄庣粏',
+ resolveRecordLabel: (record) => record?.feildValue || record?.wkTypeText || record?.ruleId || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ const buildPreviewMeta = (rows) => {
+ const now = new Date()
+ return {
+ reportDate: now.toLocaleDateString('zh-CN'),
+ printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length,
+ reportStyle: { ...SERIAL_RULE_ITEM_REPORT_STYLE }
+ }
+ }
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetSerialRuleItemMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchSerialRuleItemPage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'serial-rule-item.xlsx',
+ requestExport: (payload) =>
+ fetchExportSerialRuleItemReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildSerialRuleItemPrintRows(records),
+ buildPreviewMeta: buildPreviewMeta
+ })
+
+ const resolvedPreviewMeta = computed(() =>
+ buildSerialRuleItemReportMeta({
+ previewMeta: previewMeta.value,
+ count: previewRows.value.length,
+ orientation: previewMeta.value?.reportStyle?.orientation || SERIAL_RULE_ITEM_REPORT_STYLE.orientation
+ })
+ )
+
+ function handleSearch(params) {
+ replaceSearchParams(buildSerialRuleItemSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createSerialRuleItemSearchState())
+ resetSearchParams()
+ }
+
+ async function loadWkTypeOptions() {
+ const response = await guardRequestWithMessage(
+ fetchDictDataPage({
+ current: 1,
+ pageSize: 200,
+ dictTypeCode: 'sys_rule_item_type',
+ status: 1
+ }),
+ { records: [] },
+ { timeoutMessage: '瑙勫垯绫诲瀷鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟' }
+ )
+ const records = defaultResponseAdapter(response).records
+ wkTypeOptions.value = Array.isArray(records)
+ ? records
+ .map((item) => {
+ if (!item || typeof item !== 'object') {
+ return null
+ }
+ const value = item.value ?? item.code ?? item.dictValue ?? item.id
+ const label = item.label ?? item.name ?? item.dictLabel ?? item.value
+ if (value === void 0 || value === null || value === '') {
+ return null
+ }
+ return {
+ label: String(label ?? value),
+ value: String(value)
+ }
+ })
+ .filter(Boolean)
+ : []
+ }
+
+ onMounted(async () => {
+ await loadWkTypeOptions()
+ })
+</script>
diff --git a/rsf-design/src/views/system/serial-rule-item/modules/serial-rule-item-detail-drawer.vue b/rsf-design/src/views/system/serial-rule-item/modules/serial-rule-item-detail-drawer.vue
new file mode 100644
index 0000000..2ef4cd5
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule-item/modules/serial-rule-item-detail-drawer.vue
@@ -0,0 +1,44 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="缂栫爜瑙勫垯鏄庣粏璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="瑙勫垯涓昏〃鏍囪瘑">{{ displayData.ruleId || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瑙勫垯绫诲瀷">{{ displayData.wkTypeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀛楁鍊�">{{ displayData.feildValue || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎴彇闀垮害">{{ displayData.len ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="璧峰浣嶇疆">{{ displayData.lenStr ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎺掑簭椤哄簭">{{ displayData.sort ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ displayData.updateByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeSerialRuleItemListRow } from '../serialRuleItemPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeSerialRuleItemListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/serial-rule-item/modules/serial-rule-item-dialog.vue b/rsf-design/src/views/system/serial-rule-item/modules/serial-rule-item-dialog.vue
new file mode 100644
index 0000000..612c7f3
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule-item/modules/serial-rule-item-dialog.vue
@@ -0,0 +1,194 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="120px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildSerialRuleItemDialogModel,
+ createSerialRuleItemFormState,
+ getSerialRuleItemStatusOptions
+ } from '../serialRuleItemPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ serialRuleItemData: { type: Object, default: () => ({}) },
+ wkTypeOptions: { type: Array, default: () => [] }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createSerialRuleItemFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫缂栫爜瑙勫垯鏄庣粏' : '鏂板缂栫爜瑙勫垯鏄庣粏'))
+
+ const rules = computed(() => ({
+ ruleId: [{ required: true, message: '璇疯緭鍏ヨ鍒欎富琛ㄦ爣璇�', trigger: 'blur' }],
+ wkType: [{ required: true, message: '璇烽�夋嫨瑙勫垯绫诲瀷', trigger: 'change' }],
+ lenStr: [{ required: true, message: '璇疯緭鍏ヨ捣濮嬩綅缃�', trigger: 'blur' }],
+ sort: [{ required: true, message: '璇疯緭鍏ユ帓搴忛『搴�', trigger: 'blur' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '瑙勫垯涓昏〃鏍囪瘑',
+ key: 'ruleId',
+ type: 'number',
+ props: {
+ min: 1,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ鍒欎富琛ㄦ爣璇�',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '瑙勫垯绫诲瀷',
+ key: 'wkType',
+ type: 'select',
+ props: {
+ options: props.wkTypeOptions.length > 0 ? props.wkTypeOptions : [],
+ placeholder: '璇烽�夋嫨瑙勫垯绫诲瀷',
+ filterable: true,
+ clearable: true
+ }
+ },
+ {
+ label: '瀛楁鍊�',
+ key: 'feildValue',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ瓧娈靛��',
+ clearable: true
+ }
+ },
+ {
+ label: '鎴彇闀垮害',
+ key: 'len',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ埅鍙栭暱搴�',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '璧峰浣嶇疆',
+ key: 'lenStr',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ヨ捣濮嬩綅缃�',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '鎺掑簭椤哄簭',
+ key: 'sort',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ placeholder: '璇疯緭鍏ユ帓搴忛『搴�',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: getSerialRuleItemStatusOptions(),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: false
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createSerialRuleItemFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildSerialRuleItemDialogModel(props.serialRuleItemData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.serialRuleItemData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/serial-rule-item/serialRuleItemPage.helpers.js b/rsf-design/src/views/system/serial-rule-item/serialRuleItemPage.helpers.js
new file mode 100644
index 0000000..a4d0f5a
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule-item/serialRuleItemPage.helpers.js
@@ -0,0 +1,206 @@
+const STATUS_OPTIONS = [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+]
+
+export const SERIAL_RULE_ITEM_REPORT_TITLE = '缂栫爜瑙勫垯鏄庣粏鎶ヨ〃'
+
+export const SERIAL_RULE_ITEM_REPORT_STYLE = {
+ titleAlign: 'center',
+ titleLevel: 'strong',
+ orientation: 'landscape',
+ density: 'compact',
+ showSequence: true,
+ showBorder: true
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value, fallback = void 0) {
+ if (value === '' || value === null || value === undefined) {
+ return fallback
+ }
+ const parsed = Number(value)
+ return Number.isNaN(parsed) ? fallback : parsed
+}
+
+export function createSerialRuleItemSearchState() {
+ return {
+ condition: '',
+ ruleId: '',
+ wkType: '',
+ feildValue: '',
+ len: '',
+ lenStr: '',
+ sort: '',
+ status: '',
+ memo: '',
+ timeStart: '',
+ timeEnd: ''
+ }
+}
+
+export function createSerialRuleItemFormState() {
+ return {
+ id: void 0,
+ ruleId: void 0,
+ wkType: '',
+ feildValue: '',
+ len: void 0,
+ lenStr: void 0,
+ sort: void 0,
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getSerialRuleItemPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getSerialRuleItemStatusOptions() {
+ return STATUS_OPTIONS
+}
+
+export function getSerialRuleItemStatusMeta(status) {
+ if (status === true || Number(status) === 1) {
+ return { text: '姝e父', type: 'success', bool: true }
+ }
+ if (status === false || Number(status) === 0) {
+ return { text: '鍐荤粨', type: 'danger', bool: false }
+ }
+ return { text: '鏈煡', type: 'info', bool: false }
+}
+
+export function buildSerialRuleItemSearchParams(params = {}) {
+ const searchParams = {
+ condition: normalizeText(params.condition),
+ ruleId: normalizeNumber(params.ruleId, void 0),
+ wkType: normalizeText(params.wkType),
+ feildValue: normalizeText(params.feildValue),
+ len: normalizeNumber(params.len, void 0),
+ lenStr: normalizeNumber(params.lenStr, void 0),
+ sort: normalizeNumber(params.sort, void 0),
+ status: normalizeNumber(params.status, void 0),
+ memo: normalizeText(params.memo),
+ timeStart: normalizeText(params.timeStart),
+ timeEnd: normalizeText(params.timeEnd)
+ }
+
+ return Object.fromEntries(
+ Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+ )
+}
+
+export function buildSerialRuleItemPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildSerialRuleItemSearchParams(params)
+ }
+}
+
+export function buildSerialRuleItemDialogModel(record = {}) {
+ return {
+ ...createSerialRuleItemFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ ruleId:
+ record.ruleId !== void 0 && record.ruleId !== null && record.ruleId !== ''
+ ? Number(record.ruleId)
+ : void 0,
+ wkType: record.wkType || '',
+ feildValue: record.feildValue || '',
+ len: record.len !== void 0 && record.len !== null && record.len !== '' ? Number(record.len) : void 0,
+ lenStr:
+ record.lenStr !== void 0 && record.lenStr !== null && record.lenStr !== ''
+ ? Number(record.lenStr)
+ : void 0,
+ sort: record.sort !== void 0 && record.sort !== null && record.sort !== '' ? Number(record.sort) : void 0,
+ status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildSerialRuleItemSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ ...(formData.ruleId !== void 0 && formData.ruleId !== null && formData.ruleId !== ''
+ ? { ruleId: Number(formData.ruleId) }
+ : {}),
+ wkType: normalizeText(formData.wkType) || '',
+ feildValue: normalizeText(formData.feildValue) || '',
+ ...(formData.len !== void 0 && formData.len !== null && formData.len !== ''
+ ? { len: Number(formData.len) }
+ : {}),
+ ...(formData.lenStr !== void 0 && formData.lenStr !== null && formData.lenStr !== ''
+ ? { lenStr: Number(formData.lenStr) }
+ : {}),
+ ...(formData.sort !== void 0 && formData.sort !== null && formData.sort !== ''
+ ? { sort: Number(formData.sort) }
+ : {}),
+ status: formData.status !== void 0 && formData.status !== null ? Number(formData.status) : 1,
+ memo: normalizeText(formData.memo) || ''
+ }
+}
+
+export function normalizeSerialRuleItemListRow(record = {}) {
+ const statusMeta = getSerialRuleItemStatusMeta(record.statusBool ?? record.status)
+ return {
+ ...record,
+ ruleId: record.ruleId ?? '-',
+ wkType: record.wkType || '',
+ wkTypeText: record['wkType$'] || record.wkType || '-',
+ feildValue: record.feildValue || '-',
+ len: record.len ?? '-',
+ lenStr: record.lenStr ?? '-',
+ sort: record.sort ?? '-',
+ memo: record.memo || '',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateByLabel: record['updateBy$'] || record.updateBy || '',
+ createByLabel: record['createBy$'] || record.createBy || '',
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
+
+export function buildSerialRuleItemPrintRows(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ return records.map((record) => normalizeSerialRuleItemListRow(record))
+}
+
+export function buildSerialRuleItemReportMeta({
+ previewMeta = {},
+ count = 0,
+ titleAlign = SERIAL_RULE_ITEM_REPORT_STYLE.titleAlign,
+ titleLevel = SERIAL_RULE_ITEM_REPORT_STYLE.titleLevel,
+ orientation = SERIAL_RULE_ITEM_REPORT_STYLE.orientation,
+ density = SERIAL_RULE_ITEM_REPORT_STYLE.density,
+ showSequence = SERIAL_RULE_ITEM_REPORT_STYLE.showSequence,
+ showBorder = SERIAL_RULE_ITEM_REPORT_STYLE.showBorder
+} = {}) {
+ return {
+ reportTitle: SERIAL_RULE_ITEM_REPORT_TITLE,
+ reportDate: previewMeta.reportDate,
+ printedAt: previewMeta.printedAt,
+ operator: previewMeta.operator,
+ count,
+ reportStyle: {
+ titleAlign,
+ titleLevel,
+ orientation,
+ density,
+ showSequence,
+ showBorder
+ }
+ }
+}
diff --git a/rsf-design/src/views/system/serial-rule-item/serialRuleItemTable.columns.js b/rsf-design/src/views/system/serial-rule-item/serialRuleItemTable.columns.js
new file mode 100644
index 0000000..d51cf71
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule-item/serialRuleItemTable.columns.js
@@ -0,0 +1,103 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createSerialRuleItemTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ type: 'globalIndex',
+ label: '搴忓彿',
+ width: 72,
+ align: 'center'
+ },
+ {
+ prop: 'ruleId',
+ label: '瑙勫垯涓昏〃鏍囪瘑',
+ width: 120,
+ align: 'center'
+ },
+ {
+ prop: 'wkTypeText',
+ label: '瑙勫垯绫诲瀷',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'feildValue',
+ label: '瀛楁鍊�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'len',
+ label: '鎴彇闀垮害',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.len ?? '-'
+ },
+ {
+ prop: 'lenStr',
+ label: '璧峰浣嶇疆',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.lenStr ?? '-'
+ },
+ {
+ prop: 'sort',
+ label: '鎺掑簭椤哄簭',
+ width: 110,
+ align: 'center',
+ formatter: (row) => row.sort ?? '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/serial-rule/index.vue b/rsf-design/src/views/system/serial-rule/index.vue
new file mode 100644
index 0000000..a1d16cc
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule/index.vue
@@ -0,0 +1,223 @@
+<template>
+ <div class="serial-rule-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板缂栫爜瑙勫垯</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <SerialRuleDialog
+ v-model:visible="dialogVisible"
+ :serial-rule-data="currentSerialRuleData"
+ @submit="handleDialogSubmit"
+ />
+
+ <SerialRuleDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useCrudPage } from '@/views/system/common/useCrudPage'
+ import {
+ fetchDeleteSerialRule,
+ fetchGetSerialRuleDetail,
+ fetchSaveSerialRule,
+ fetchSerialRulePage,
+ fetchUpdateSerialRule
+ } from '@/api/system-manage'
+ import SerialRuleDialog from './modules/serial-rule-dialog.vue'
+ import SerialRuleDetailDrawer from './modules/serial-rule-detail-drawer.vue'
+ import { createSerialRuleTableColumns } from './serialRuleTable.columns'
+ import {
+ buildSerialRuleDialogModel,
+ buildSerialRulePageQueryParams,
+ buildSerialRuleSavePayload,
+ buildSerialRuleSearchParams,
+ createSerialRuleSearchState,
+ getSerialRulePaginationKey,
+ getSerialRuleResetOptions,
+ normalizeSerialRuleListRow
+ } from './serialRulePage.helpers'
+
+ defineOptions({ name: 'SerialRule' })
+
+ const { hasAuth } = useAuth()
+ const searchForm = ref(createSerialRuleSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鍙锋垨鍚嶇О'
+ }
+ },
+ {
+ label: '缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ紪鍙�'
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ悕绉�'
+ }
+ },
+ {
+ label: '閲嶇疆瑙勫垯',
+ key: 'reset',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getSerialRuleResetOptions()
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeSerialRuleListRow(await fetchGetSerialRuleDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇缂栫爜瑙勫垯璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentSerialRuleData.value = buildSerialRuleDialogModel(await fetchGetSerialRuleDetail(row.id))
+ dialogVisible.value = true
+ dialogType.value = 'edit'
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇缂栫爜瑙勫垯璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchSerialRulePage,
+ apiParams: buildSerialRulePageQueryParams(searchForm.value),
+ paginationKey: getSerialRulePaginationKey(),
+ columnsFactory: () =>
+ createSerialRuleTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeSerialRuleListRow(item))
+ }
+ }
+ })
+
+ const {
+ dialogVisible,
+ dialogType,
+ currentRecord: currentSerialRuleData,
+ selectedRows,
+ handleSelectionChange,
+ showDialog,
+ handleDialogSubmit,
+ handleDelete,
+ handleBatchDelete
+ } = useCrudPage({
+ createEmptyModel: () => buildSerialRuleDialogModel(),
+ buildEditModel: (record) => buildSerialRuleDialogModel(record),
+ buildSavePayload: (formData) => buildSerialRuleSavePayload(formData),
+ saveRequest: fetchSaveSerialRule,
+ updateRequest: fetchUpdateSerialRule,
+ deleteRequest: fetchDeleteSerialRule,
+ entityName: '缂栫爜瑙勫垯',
+ resolveRecordLabel: (record) => record?.code || record?.name || record?.id,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ })
+ handleDeleteAction = handleDelete
+
+ function handleSearch(params) {
+ replaceSearchParams(buildSerialRuleSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createSerialRuleSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/serial-rule/modules/serial-rule-detail-drawer.vue b/rsf-design/src/views/system/serial-rule/modules/serial-rule-detail-drawer.vue
new file mode 100644
index 0000000..6bc70c9
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule/modules/serial-rule-detail-drawer.vue
@@ -0,0 +1,45 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="缂栫爜瑙勫垯璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="缂栧彿">{{ displayData.code || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍚嶇О">{{ displayData.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒嗛殧绗�">{{ displayData.delimit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲嶇疆瑙勫垯">{{ displayData.resetText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閲嶇疆渚濊禆">{{ displayData.resetDep || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="褰撳墠鍊�">{{ displayData.currValue ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�杩戠敓鎴愮紪鐮�">{{ displayData.lastCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊浜�">{{ displayData.updateByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓浜�">{{ displayData.createByLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeSerialRuleListRow } from '../serialRulePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeSerialRuleListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/serial-rule/modules/serial-rule-dialog.vue b/rsf-design/src/views/system/serial-rule/modules/serial-rule-dialog.vue
new file mode 100644
index 0000000..513a942
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule/modules/serial-rule-dialog.vue
@@ -0,0 +1,195 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ width="820px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildSerialRuleDialogModel,
+ createSerialRuleFormState,
+ getSerialRuleResetOptions
+ } from '../serialRulePage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ serialRuleData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createSerialRuleFormState())
+
+ const isEdit = computed(() => Boolean(form.id))
+ const dialogTitle = computed(() => (isEdit.value ? '缂栬緫缂栫爜瑙勫垯' : '鏂板缂栫爜瑙勫垯'))
+
+ const rules = computed(() => ({
+ code: [{ required: true, message: '璇疯緭鍏ョ紪鍙�', trigger: 'blur' }],
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }],
+ reset: [{ required: true, message: '璇烽�夋嫨閲嶇疆瑙勫垯', trigger: 'change' }]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '缂栧彿',
+ key: 'code',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ョ紪鍙�',
+ clearable: true,
+ disabled: isEdit.value
+ }
+ },
+ {
+ label: '鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ悕绉�',
+ clearable: true
+ }
+ },
+ {
+ label: '鍒嗛殧绗�',
+ key: 'delimit',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ュ垎闅旂',
+ clearable: true
+ }
+ },
+ {
+ label: '閲嶇疆瑙勫垯',
+ key: 'reset',
+ type: 'select',
+ props: {
+ options: getSerialRuleResetOptions(),
+ placeholder: '璇烽�夋嫨閲嶇疆瑙勫垯'
+ }
+ },
+ {
+ label: '閲嶇疆渚濊禆',
+ key: 'resetDep',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ラ噸缃緷璧�',
+ clearable: true
+ }
+ },
+ {
+ label: '褰撳墠鍊�',
+ key: 'currValue',
+ type: 'number',
+ props: {
+ min: 0,
+ controlsPosition: 'right',
+ style: { width: '100%' }
+ }
+ },
+ {
+ label: '鏈�杩戠敓鎴愮紪鐮�',
+ key: 'lastCode',
+ type: 'input',
+ props: {
+ placeholder: '璇疯緭鍏ユ渶杩戠敓鎴愮紪鐮�',
+ clearable: true
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ],
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createSerialRuleFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildSerialRuleDialogModel(props.serialRuleData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.serialRuleData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/serial-rule/serialRulePage.helpers.js b/rsf-design/src/views/system/serial-rule/serialRulePage.helpers.js
new file mode 100644
index 0000000..e21e761
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule/serialRulePage.helpers.js
@@ -0,0 +1,124 @@
+const SERIAL_RULE_RESET_OPTIONS = [
+ { label: '骞撮噸缃�', value: 'year' },
+ { label: '鏈堥噸缃�', value: 'month' },
+ { label: '鏃ラ噸缃�', value: 'day' }
+]
+
+export function createSerialRuleSearchState() {
+ return {
+ condition: '',
+ code: '',
+ name: '',
+ reset: '',
+ status: ''
+ }
+}
+
+export function createSerialRuleFormState() {
+ return {
+ id: null,
+ code: '',
+ name: '',
+ delimit: '',
+ reset: '',
+ resetDep: '',
+ currValue: 0,
+ lastCode: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getSerialRulePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getSerialRuleResetOptions() {
+ return SERIAL_RULE_RESET_OPTIONS
+}
+
+export function getSerialRuleResetLabel(value) {
+ return SERIAL_RULE_RESET_OPTIONS.find((item) => item.value === value)?.label || value || '-'
+}
+
+export function getSerialRuleStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '姝e父', type: 'success', bool: true }
+ : { text: '鍐荤粨', type: 'danger', bool: false }
+}
+
+export function buildSerialRuleSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ code: String(params.code || '').trim(),
+ name: String(params.name || '').trim(),
+ ...(params.reset ? { reset: String(params.reset) } : {}),
+ ...(params.status !== '' && params.status !== null && params.status !== undefined
+ ? { status: Number(params.status) }
+ : {})
+ }
+}
+
+export function buildSerialRulePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildSerialRuleSearchParams(params)
+ }
+}
+
+export function buildSerialRuleDialogModel(record = {}) {
+ return {
+ ...createSerialRuleFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ code: record.code || '',
+ name: record.name || '',
+ delimit: record.delimit || '',
+ reset: record.reset || '',
+ resetDep: record.resetDep || '',
+ currValue: record.currValue !== undefined && record.currValue !== null ? Number(record.currValue) : 0,
+ lastCode: record.lastCode || '',
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildSerialRuleSavePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ code: String(formData.code || '').trim(),
+ name: String(formData.name || '').trim(),
+ delimit: String(formData.delimit || '').trim(),
+ reset: String(formData.reset || '').trim(),
+ resetDep: String(formData.resetDep || '').trim(),
+ currValue: Number(formData.currValue || 0),
+ lastCode: String(formData.lastCode || '').trim(),
+ status: Number(formData.status ?? 1),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function normalizeSerialRuleListRow(record = {}) {
+ const statusMeta = getSerialRuleStatusMeta(record.status)
+ return {
+ ...record,
+ code: record.code || '',
+ name: record.name || '',
+ delimit: record.delimit || '-',
+ resetText: record['reset$'] || getSerialRuleResetLabel(record.reset),
+ resetDep: record.resetDep || '-',
+ currValue: record.currValue ?? 0,
+ lastCode: record.lastCode || '-',
+ memo: record.memo || '',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateByLabel: record['updateBy$'] || '',
+ createByLabel: record['createBy$'] || '',
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
diff --git a/rsf-design/src/views/system/serial-rule/serialRuleTable.columns.js b/rsf-design/src/views/system/serial-rule/serialRuleTable.columns.js
new file mode 100644
index 0000000..02faecc
--- /dev/null
+++ b/rsf-design/src/views/system/serial-rule/serialRuleTable.columns.js
@@ -0,0 +1,92 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createSerialRuleTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'code',
+ label: '缂栧彿',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'name',
+ label: '鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'delimit',
+ label: '鍒嗛殧绗�',
+ width: 100,
+ formatter: (row) => row.delimit || '-'
+ },
+ {
+ prop: 'resetText',
+ label: '閲嶇疆瑙勫垯',
+ minWidth: 120,
+ formatter: (row) => row.resetText || '-'
+ },
+ {
+ prop: 'resetDep',
+ label: '閲嶇疆渚濊禆',
+ minWidth: 120,
+ formatter: (row) => row.resetDep || '-'
+ },
+ {
+ prop: 'currValue',
+ label: '褰撳墠鍊�',
+ width: 100,
+ formatter: (row) => row.currValue ?? 0
+ },
+ {
+ prop: 'lastCode',
+ label: '鏈�杩戠敓鎴愮紪鐮�',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.lastCode || '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/subsystem-flow-template/index.vue b/rsf-design/src/views/system/subsystem-flow-template/index.vue
new file mode 100644
index 0000000..8f83909
--- /dev/null
+++ b/rsf-design/src/views/system/subsystem-flow-template/index.vue
@@ -0,0 +1,185 @@
+<template>
+ <div class="subsystem-flow-template-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <SubsystemFlowTemplateDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import {
+ fetchExportSubsystemFlowTemplateReport,
+ fetchGetSubsystemFlowTemplateDetail,
+ fetchGetSubsystemFlowTemplateMany,
+ fetchSubsystemFlowTemplatePage
+ } from '@/api/subsystem-flow-template'
+ import {
+ buildSubsystemFlowTemplatePageQueryParams,
+ buildSubsystemFlowTemplatePrintRows,
+ buildSubsystemFlowTemplateSearchParams,
+ createSubsystemFlowTemplateSearchState,
+ getSubsystemFlowTemplatePaginationKey,
+ getSubsystemFlowTemplateReportColumns,
+ normalizeSubsystemFlowTemplateRow,
+ SUBSYSTEM_FLOW_TEMPLATE_REPORT_TITLE
+ } from './subsystemFlowTemplatePage.helpers'
+ import { createSubsystemFlowTemplateTableColumns } from './subsystemFlowTemplateTable.columns'
+ import SubsystemFlowTemplateDetailDrawer from './modules/subsystem-flow-template-detail-drawer.vue'
+
+ defineOptions({ name: 'SubsystemFlowTemplate' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createSubsystemFlowTemplateSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = SUBSYSTEM_FLOW_TEMPLATE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildSubsystemFlowTemplateSearchParams(searchForm.value))
+ const reportColumns = getSubsystemFlowTemplateReportColumns()
+
+ const searchItems = computed(() => [
+ { label: '鍏抽敭瀛�', key: 'condition', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋嬬紪鐮�/娴佺▼鍚嶇О' } },
+ { label: '娴佺▼缂栫爜', key: 'flowCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋嬬紪鐮�' } },
+ { label: '娴佺▼鍚嶇О', key: 'flowName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ユ祦绋嬪悕绉�' } },
+ { label: '绯荤粺缂栫爜', key: 'systemCode', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ郴缁熺紪鐮�' } },
+ { label: '绯荤粺鍚嶇О', key: 'systemName', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ョ郴缁熷悕绉�' } },
+ { label: '鑺傜偣绫诲瀷', key: 'nodeType', type: 'input', props: { clearable: true, placeholder: '璇疯緭鍏ヨ妭鐐圭被鍨�' } },
+ { label: '寮�濮嬫棩鏈�', key: 'timeStart', type: 'date', props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' } },
+ { label: '缁撴潫鏃ユ湡', key: 'timeEnd', type: 'date', props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' } }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchSubsystemFlowTemplatePage,
+ apiParams: buildSubsystemFlowTemplatePageQueryParams(searchForm.value),
+ paginationKey: getSubsystemFlowTemplatePaginationKey(),
+ columnsFactory: () => createSubsystemFlowTemplateTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeSubsystemFlowTemplateRow(item)) : []
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetSubsystemFlowTemplateMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchSubsystemFlowTemplatePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'subsystem-flow-template.xlsx',
+ requestExport: (payload) =>
+ fetchExportSubsystemFlowTemplateReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildSubsystemFlowTemplatePrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetSubsystemFlowTemplateDetail(id)
+ detailData.value = normalizeSubsystemFlowTemplateRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildSubsystemFlowTemplateSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createSubsystemFlowTemplateSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/subsystem-flow-template/modules/subsystem-flow-template-detail-drawer.vue b/rsf-design/src/views/system/subsystem-flow-template/modules/subsystem-flow-template-detail-drawer.vue
new file mode 100644
index 0000000..3ba9724
--- /dev/null
+++ b/rsf-design/src/views/system/subsystem-flow-template/modules/subsystem-flow-template-detail-drawer.vue
@@ -0,0 +1,33 @@
+<template>
+ <ElDrawer :model-value="visible" title="瀛愮郴缁熸祦绋嬫ā鏉胯鎯�" size="620px" @close="emit('update:visible', false)">
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="娴佺▼缂栫爜">{{ detail.flowCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="娴佺▼鍚嶇О">{{ detail.flowName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绯荤粺缂栫爜">{{ detail.systemCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绯荤粺鍚嶇О">{{ detail.systemName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣绫诲瀷">{{ detail.nodeType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗堟湰">{{ detail.version || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="褰撳墠鐗堟湰">{{ detail.isCurrentText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瓒呮椂绛栫暐">{{ detail.timeoutStrategy || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瓒呮椂(绉�)">{{ detail.timeoutSeconds || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏈�澶ч噸璇曟鏁�">{{ detail.maxRetryTimes || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineProps({
+ visible: {
+ type: Boolean,
+ default: false
+ },
+ detail: {
+ type: Object,
+ default: () => ({})
+ }
+ })
+
+ const emit = defineEmits(['update:visible'])
+</script>
diff --git a/rsf-design/src/views/system/subsystem-flow-template/subsystemFlowTemplatePage.helpers.js b/rsf-design/src/views/system/subsystem-flow-template/subsystemFlowTemplatePage.helpers.js
new file mode 100644
index 0000000..062bc7b
--- /dev/null
+++ b/rsf-design/src/views/system/subsystem-flow-template/subsystemFlowTemplatePage.helpers.js
@@ -0,0 +1,163 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const SUBSYSTEM_FLOW_TEMPLATE_REPORT_TITLE = '瀛愮郴缁熸祦绋嬫ā鏉挎姤琛�'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function normalizeBoolText(value) {
+ const numericValue = Number(value)
+ if (numericValue === 1) return '鏄�'
+ if (numericValue === 0) return '鍚�'
+ return '--'
+}
+
+export function createSubsystemFlowTemplateSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ flowCode: '',
+ flowName: '',
+ systemCode: '',
+ systemName: '',
+ nodeType: '',
+ version: '',
+ isCurrent: '',
+ effectiveTime: '',
+ timeoutStrategy: '',
+ timeoutSeconds: '',
+ maxRetryTimes: '',
+ needNotify: '',
+ notifyTemplate: '',
+ remark: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getSubsystemFlowTemplatePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildSubsystemFlowTemplateSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'flowCode',
+ 'flowName',
+ 'systemCode',
+ 'systemName',
+ 'nodeType',
+ 'effectiveTime',
+ 'timeoutStrategy',
+ 'notifyTemplate',
+ 'remark',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;['version', 'isCurrent', 'timeoutSeconds', 'maxRetryTimes', 'needNotify', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildSubsystemFlowTemplatePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildSubsystemFlowTemplateSearchParams(params)
+ }
+}
+
+export function normalizeSubsystemFlowTemplateRow(record = {}) {
+ const statusMeta = STATUS_META[Number(record.status)] || STATUS_META[0]
+ return {
+ ...record,
+ id: record.id ?? '--',
+ flowCode: normalizeText(record.flowCode) || '--',
+ flowName: normalizeText(record.flowName) || '--',
+ systemCode: normalizeText(record.systemCode) || '--',
+ systemName: normalizeText(record.systemName) || '--',
+ nodeType: normalizeText(record.nodeType) || '--',
+ version: record.version ?? '--',
+ isCurrentText: normalizeBoolText(record.isCurrent),
+ timeoutStrategy: normalizeText(record.timeoutStrategy) || '--',
+ timeoutSeconds: record.timeoutSeconds ?? '--',
+ maxRetryTimes: record.maxRetryTimes ?? '--',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function getSubsystemFlowTemplateReportColumns() {
+ return [
+ { prop: 'flowCode', label: '娴佺▼缂栫爜' },
+ { prop: 'flowName', label: '娴佺▼鍚嶇О' },
+ { prop: 'systemCode', label: '绯荤粺缂栫爜' },
+ { prop: 'systemName', label: '绯荤粺鍚嶇О' },
+ { prop: 'nodeType', label: '鑺傜偣绫诲瀷' },
+ { prop: 'version', label: '鐗堟湰' },
+ { prop: 'isCurrentText', label: '褰撳墠鐗堟湰' },
+ { prop: 'timeoutStrategy', label: '瓒呮椂绛栫暐' },
+ { prop: 'timeoutSeconds', label: '瓒呮椂(绉�)' },
+ { prop: 'maxRetryTimes', label: '鏈�澶ч噸璇曟鏁�' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildSubsystemFlowTemplatePrintRows(records = []) {
+ return (Array.isArray(records) ? records : []).map((record) => {
+ const row = normalizeSubsystemFlowTemplateRow(record)
+ return {
+ flowCode: row.flowCode,
+ flowName: row.flowName,
+ systemCode: row.systemCode,
+ systemName: row.systemName,
+ nodeType: row.nodeType,
+ version: row.version,
+ isCurrentText: row.isCurrentText,
+ timeoutStrategy: row.timeoutStrategy,
+ timeoutSeconds: row.timeoutSeconds,
+ maxRetryTimes: row.maxRetryTimes,
+ statusText: row.statusText,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/subsystem-flow-template/subsystemFlowTemplateTable.columns.js b/rsf-design/src/views/system/subsystem-flow-template/subsystemFlowTemplateTable.columns.js
new file mode 100644
index 0000000..e465177
--- /dev/null
+++ b/rsf-design/src/views/system/subsystem-flow-template/subsystemFlowTemplateTable.columns.js
@@ -0,0 +1,48 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createSubsystemFlowTemplateTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ { prop: 'flowCode', label: '娴佺▼缂栫爜', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'flowName', label: '娴佺▼鍚嶇О', minWidth: 160, showOverflowTooltip: true },
+ { prop: 'systemCode', label: '绯荤粺缂栫爜', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'systemName', label: '绯荤粺鍚嶇О', minWidth: 140, showOverflowTooltip: true },
+ { prop: 'nodeType', label: '鑺傜偣绫诲瀷', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'version', label: '鐗堟湰', width: 90, align: 'center' },
+ { prop: 'isCurrentText', label: '褰撳墠鐗堟湰', width: 100, align: 'center' },
+ { prop: 'timeoutStrategy', label: '瓒呮椂绛栫暐', minWidth: 120, showOverflowTooltip: true },
+ { prop: 'timeoutSeconds', label: '瓒呮椂(绉�)', width: 100, align: 'right' },
+ { prop: 'maxRetryTimes', label: '鏈�澶ч噸璇曟鏁�', width: 120, align: 'right' },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.statusType || 'info',
+ effect: 'light'
+ },
+ () => row?.statusText || '--'
+ )
+ },
+ { prop: 'memo', label: '澶囨敞', minWidth: 180, showOverflowTooltip: true },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/task-instance-node/index.vue b/rsf-design/src/views/system/task-instance-node/index.vue
new file mode 100644
index 0000000..aea1479
--- /dev/null
+++ b/rsf-design/src/views/system/task-instance-node/index.vue
@@ -0,0 +1,233 @@
+<template>
+ <div class="task-instance-node-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <TaskInstanceNodeDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ fetchExportTaskInstanceNodeReport,
+ fetchGetTaskInstanceNodeDetail,
+ fetchGetTaskInstanceNodeMany,
+ fetchTaskInstanceNodePage
+ } from '@/api/task-instance-node'
+ import {
+ buildTaskInstanceNodePageQueryParams,
+ buildTaskInstanceNodePrintRows,
+ buildTaskInstanceNodeSearchParams,
+ createTaskInstanceNodeSearchState,
+ getTaskInstanceNodePaginationKey,
+ getTaskInstanceNodeReportColumns,
+ normalizeTaskInstanceNodeRow,
+ TASK_INSTANCE_NODE_REPORT_TITLE
+ } from './taskInstanceNodePage.helpers'
+ import { createTaskInstanceNodeTableColumns } from './taskInstanceNodeTable.columns'
+ import TaskInstanceNodeDetailDrawer from './modules/task-instance-node-detail-drawer.vue'
+
+ defineOptions({ name: 'TaskInstanceNode' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createTaskInstanceNodeSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = TASK_INSTANCE_NODE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildTaskInstanceNodeSearchParams(searchForm.value))
+ const reportColumns = getTaskInstanceNodeReportColumns()
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ换鍔″彿/鑺傜偣缂栫爜/鑺傜偣鍚嶇О'
+ }
+ },
+ {
+ label: '浠诲姟鍙�',
+ key: 'taskNo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ换鍔″彿'
+ }
+ },
+ {
+ label: '鑺傜偣缂栫爜',
+ key: 'nodeCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ妭鐐圭紪鐮�'
+ }
+ },
+ {
+ label: '鑺傜偣鍚嶇О',
+ key: 'nodeName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ妭鐐瑰悕绉�'
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchTaskInstanceNodePage,
+ apiParams: buildTaskInstanceNodePageQueryParams(searchForm.value),
+ paginationKey: getTaskInstanceNodePaginationKey(),
+ columnsFactory: () => createTaskInstanceNodeTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeTaskInstanceNodeRow(item)) : []
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetTaskInstanceNodeMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchTaskInstanceNodePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'task-instance-node.xlsx',
+ requestExport: (payload) =>
+ fetchExportTaskInstanceNodeReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTaskInstanceNodePrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetTaskInstanceNodeDetail(id)
+ detailData.value = normalizeTaskInstanceNodeRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTaskInstanceNodeSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTaskInstanceNodeSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/task-instance-node/modules/task-instance-node-detail-drawer.vue b/rsf-design/src/views/system/task-instance-node/modules/task-instance-node-detail-drawer.vue
new file mode 100644
index 0000000..e6888c4
--- /dev/null
+++ b/rsf-design/src/views/system/task-instance-node/modules/task-instance-node-detail-drawer.vue
@@ -0,0 +1,45 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟瀹炰緥鑺傜偣璇︽儏"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="浠诲姟ID">{{ detail.taskId ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="浠诲姟鍙�">{{ detail.taskNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣缂栫爜">{{ detail.nodeCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣鍚嶇О">{{ detail.nodeName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣椤哄簭">{{ detail.nodeOrder ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绯荤粺鍚嶇О">{{ detail.systemName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц缁撴灉" :span="2">{{ detail.executeResult || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="閿欒淇℃伅" :span="2">{{ detail.errorMessage || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹為檯寮�濮嬫椂闂�">{{ detail.actualStartTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹為檯缁撴潫鏃堕棿">{{ detail.actualEndTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑰楁椂(绉�)">{{ detail.durationSeconds ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'TaskInstanceNodeDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/task-instance-node/taskInstanceNodePage.helpers.js b/rsf-design/src/views/system/task-instance-node/taskInstanceNodePage.helpers.js
new file mode 100644
index 0000000..19f3ae3
--- /dev/null
+++ b/rsf-design/src/views/system/task-instance-node/taskInstanceNodePage.helpers.js
@@ -0,0 +1,176 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const TASK_INSTANCE_NODE_REPORT_TITLE = '浠诲姟瀹炰緥鑺傜偣鎶ヨ〃'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function normalizeDateTime(value) {
+ return normalizeText(value) || '--'
+}
+
+export function createTaskInstanceNodeSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ taskId: '',
+ taskNo: '',
+ nodeOrder: '',
+ nodeCode: '',
+ nodeName: '',
+ nodeType: '',
+ systemCode: '',
+ systemName: '',
+ executeParams: '',
+ executeResult: '',
+ errorCode: '',
+ errorMessage: '',
+ estimatedStartTime: '',
+ actualStartTime: '',
+ actualEndTime: '',
+ timeoutAt: '',
+ durationSeconds: '',
+ retryTimes: '',
+ maxRetryTimes: '',
+ dependsOnNodes: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getTaskInstanceNodePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildTaskInstanceNodeSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'taskNo',
+ 'nodeCode',
+ 'nodeName',
+ 'nodeType',
+ 'systemCode',
+ 'systemName',
+ 'executeParams',
+ 'executeResult',
+ 'errorCode',
+ 'errorMessage',
+ 'estimatedStartTime',
+ 'actualStartTime',
+ 'actualEndTime',
+ 'timeoutAt',
+ 'dependsOnNodes',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;['taskId', 'nodeOrder', 'durationSeconds', 'retryTimes', 'maxRetryTimes', 'status'].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildTaskInstanceNodePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTaskInstanceNodeSearchParams(params)
+ }
+}
+
+export function normalizeTaskInstanceNodeRow(record = {}) {
+ const statusMeta = STATUS_META[Number(record.status)] || STATUS_META[0]
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ taskId: record.taskId ?? '--',
+ taskNo: normalizeText(record.taskNo) || '--',
+ nodeOrder: record.nodeOrder ?? '--',
+ nodeCode: normalizeText(record.nodeCode) || '--',
+ nodeName: normalizeText(record.nodeName) || '--',
+ systemName: normalizeText(record.systemName) || '--',
+ executeResult: normalizeText(record.executeResult) || '--',
+ errorMessage: normalizeText(record.errorMessage) || '--',
+ actualStartTimeText: normalizeDateTime(record['actualStartTime$'] || record.actualStartTimeText || record.actualStartTime),
+ actualEndTimeText: normalizeDateTime(record['actualEndTime$'] || record.actualEndTimeText || record.actualEndTime),
+ durationSeconds: record.durationSeconds ?? '--',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ memo: normalizeText(record.memo) || '--'
+ }
+}
+
+export function getTaskInstanceNodeReportColumns() {
+ return [
+ { prop: 'taskId', label: '浠诲姟ID' },
+ { prop: 'taskNo', label: '浠诲姟鍙�' },
+ { prop: 'nodeOrder', label: '鑺傜偣椤哄簭' },
+ { prop: 'nodeCode', label: '鑺傜偣缂栫爜' },
+ { prop: 'nodeName', label: '鑺傜偣鍚嶇О' },
+ { prop: 'systemName', label: '绯荤粺鍚嶇О' },
+ { prop: 'executeResult', label: '鎵ц缁撴灉' },
+ { prop: 'errorMessage', label: '閿欒淇℃伅' },
+ { prop: 'actualStartTimeText', label: '瀹為檯寮�濮嬫椂闂�' },
+ { prop: 'actualEndTimeText', label: '瀹為檯缁撴潫鏃堕棿' },
+ { prop: 'durationSeconds', label: '鑰楁椂(绉�)' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildTaskInstanceNodePrintRows(records = []) {
+ return (Array.isArray(records) ? records : []).map((record) => {
+ const row = normalizeTaskInstanceNodeRow(record)
+ return {
+ taskId: row.taskId,
+ taskNo: row.taskNo,
+ nodeOrder: row.nodeOrder,
+ nodeCode: row.nodeCode,
+ nodeName: row.nodeName,
+ systemName: row.systemName,
+ executeResult: row.executeResult,
+ errorMessage: row.errorMessage,
+ actualStartTimeText: row.actualStartTimeText,
+ actualEndTimeText: row.actualEndTimeText,
+ durationSeconds: row.durationSeconds,
+ statusText: row.statusText,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/task-instance-node/taskInstanceNodeTable.columns.js b/rsf-design/src/views/system/task-instance-node/taskInstanceNodeTable.columns.js
new file mode 100644
index 0000000..e2bf251
--- /dev/null
+++ b/rsf-design/src/views/system/task-instance-node/taskInstanceNodeTable.columns.js
@@ -0,0 +1,97 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createTaskInstanceNodeTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'taskId',
+ label: '浠诲姟ID',
+ minWidth: 110,
+ align: 'right'
+ },
+ {
+ prop: 'taskNo',
+ label: '浠诲姟鍙�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'nodeOrder',
+ label: '鑺傜偣椤哄簭',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'nodeCode',
+ label: '鑺傜偣缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'nodeName',
+ label: '鑺傜偣鍚嶇О',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'systemName',
+ label: '绯荤粺鍚嶇О',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'executeResult',
+ label: '鎵ц缁撴灉',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'actualStartTimeText',
+ label: '瀹為檯寮�濮嬫椂闂�',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'actualEndTimeText',
+ label: '瀹為檯缁撴潫鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.statusType || 'info',
+ effect: 'light'
+ },
+ () => row?.statusText || '--'
+ )
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/task-instance/index.vue b/rsf-design/src/views/system/task-instance/index.vue
new file mode 100644
index 0000000..19dd2c1
--- /dev/null
+++ b/rsf-design/src/views/system/task-instance/index.vue
@@ -0,0 +1,242 @@
+<template>
+ <div class="task-instance-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ListExportPrint
+ class="inline-flex"
+ :preview-visible="previewVisible"
+ @update:previewVisible="handlePreviewVisibleChange"
+ :report-title="reportTitle"
+ :selected-rows="selectedRows"
+ :query-params="reportQueryParams"
+ :columns="reportColumns"
+ :preview-rows="previewRows"
+ :preview-meta="previewMeta"
+ :total="pagination.total"
+ :disabled="loading"
+ @export="handleExport"
+ @print="handlePrint"
+ />
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <TaskInstanceDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import { useTable } from '@/hooks/core/useTable'
+ import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
+ import ListExportPrint from '@/components/biz/list-export-print/index.vue'
+ import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+ import {
+ fetchExportTaskInstanceReport,
+ fetchGetTaskInstanceDetail,
+ fetchGetTaskInstanceMany,
+ fetchTaskInstancePage
+ } from '@/api/task-instance'
+ import {
+ buildTaskInstancePageQueryParams,
+ buildTaskInstancePrintRows,
+ buildTaskInstanceSearchParams,
+ createTaskInstanceSearchState,
+ getTaskInstancePaginationKey,
+ getTaskInstanceReportColumns,
+ normalizeTaskInstanceRow,
+ TASK_INSTANCE_REPORT_TITLE
+ } from './taskInstancePage.helpers'
+ import { createTaskInstanceTableColumns } from './taskInstanceTable.columns'
+ import TaskInstanceDetailDrawer from './modules/task-instance-detail-drawer.vue'
+
+ defineOptions({ name: 'TaskInstance' })
+
+ const userStore = useUserStore()
+ const searchForm = ref(createTaskInstanceSearchState())
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const selectedRows = ref([])
+ const reportTitle = TASK_INSTANCE_REPORT_TITLE
+ const reportQueryParams = computed(() => buildTaskInstanceSearchParams(searchForm.value))
+ const reportColumns = getTaskInstanceReportColumns()
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ换鍔″彿/涓氬姟鍗曞彿/鑺傜偣鍚�'
+ }
+ },
+ {
+ label: '浠诲姟鍙�',
+ key: 'taskNo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ换鍔″彿'
+ }
+ },
+ {
+ label: '涓氬姟鍗曞彿',
+ key: 'bizNo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ笟鍔″崟鍙�'
+ }
+ },
+ {
+ label: '涓氬姟绫诲瀷',
+ key: 'bizType',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�'
+ }
+ },
+ {
+ label: '褰撳墠鑺傜偣',
+ key: 'currentNodeName',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ綋鍓嶈妭鐐�'
+ }
+ },
+ {
+ label: '寮�濮嬫棩鏈�',
+ key: 'timeStart',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ },
+ {
+ label: '缁撴潫鏃ユ湡',
+ key: 'timeEnd',
+ type: 'date',
+ props: {
+ clearable: true,
+ valueFormat: 'YYYY-MM-DD',
+ type: 'date'
+ }
+ }
+ ])
+
+ function openDetail(row) {
+ detailDrawerVisible.value = true
+ loadDetail(row.id, row)
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchTaskInstancePage,
+ apiParams: buildTaskInstancePageQueryParams(searchForm.value),
+ paginationKey: getTaskInstancePaginationKey(),
+ columnsFactory: () => createTaskInstanceTableColumns({ handleView: openDetail })
+ },
+ transform: {
+ dataTransformer: (records) =>
+ Array.isArray(records) ? records.map((item) => normalizeTaskInstanceRow(item)) : []
+ }
+ })
+
+ const resolvePrintRecords = async (payload) => {
+ if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
+ return defaultResponseAdapter(await fetchGetTaskInstanceMany(payload.ids)).records
+ }
+ return defaultResponseAdapter(
+ await fetchTaskInstancePage({
+ ...reportQueryParams.value,
+ current: 1,
+ pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
+ })
+ ).records
+ }
+
+ const {
+ previewVisible,
+ previewRows,
+ previewMeta,
+ handlePreviewVisibleChange,
+ handleExport,
+ handlePrint
+ } = usePrintExportPage({
+ downloadFileName: 'task-instance.xlsx',
+ requestExport: (payload) =>
+ fetchExportTaskInstanceReport(payload, {
+ headers: {
+ Authorization: userStore.accessToken || ''
+ }
+ }),
+ resolvePrintRecords,
+ buildPreviewRows: (records) => buildTaskInstancePrintRows(records),
+ buildPreviewMeta: (rows) => ({
+ reportTitle,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+ count: rows.length
+ })
+ })
+
+ async function loadDetail(id, fallback) {
+ const detail = await fetchGetTaskInstanceDetail(id)
+ detailData.value = normalizeTaskInstanceRow({
+ ...fallback,
+ ...detail
+ })
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTaskInstanceSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTaskInstanceSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/task-instance/modules/task-instance-detail-drawer.vue b/rsf-design/src/views/system/task-instance/modules/task-instance-detail-drawer.vue
new file mode 100644
index 0000000..b8be701
--- /dev/null
+++ b/rsf-design/src/views/system/task-instance/modules/task-instance-detail-drawer.vue
@@ -0,0 +1,45 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="浠诲姟瀹炰緥璇︽儏"
+ size="72%"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElScrollbar class="h-[calc(100vh-120px)]">
+ <div class="flex min-h-full flex-col gap-4 pr-2">
+ <ElDescriptions :column="4" border>
+ <ElDescriptionsItem label="浠诲姟鍙�">{{ detail.taskNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟鍗曞彿">{{ detail.bizNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="涓氬姟绫诲瀷">{{ detail.bizType || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="妯℃澘缂栫爜">{{ detail.templateCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏉ユ簮缂栫爜">{{ detail.sourceCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐩爣缂栫爜">{{ detail.targetCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="褰撳墠鑺傜偣">{{ detail.currentNodeName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鑺傜偣杩涘害">{{ detail.progressRate ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁撴灉淇℃伅" :span="2">{{ detail.resultMessage || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="寮�濮嬫椂闂�">{{ detail.startTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="缁撴潫鏃堕棿">{{ detail.endTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineOptions({ name: 'TaskInstanceDetailDrawer' })
+
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/task-instance/taskInstancePage.helpers.js b/rsf-design/src/views/system/task-instance/taskInstancePage.helpers.js
new file mode 100644
index 0000000..3388ece
--- /dev/null
+++ b/rsf-design/src/views/system/task-instance/taskInstancePage.helpers.js
@@ -0,0 +1,204 @@
+const STATUS_META = {
+ 1: { text: '姝e父', type: 'success' },
+ 0: { text: '鍐荤粨', type: 'info' }
+}
+
+export const TASK_INSTANCE_REPORT_TITLE = '浠诲姟瀹炰緥鎶ヨ〃'
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return null
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : null
+}
+
+function normalizeDateTime(value) {
+ return normalizeText(value) || '--'
+}
+
+export function createTaskInstanceSearchState() {
+ return {
+ condition: '',
+ timeStart: '',
+ timeEnd: '',
+ taskNo: '',
+ bizNo: '',
+ bizType: '',
+ templateId: '',
+ templateCode: '',
+ templateVersion: '',
+ sourceInfo: '',
+ targetInfo: '',
+ sourceCode: '',
+ targetCode: '',
+ plannedPath: '',
+ actualPath: '',
+ priority: '',
+ timeoutAt: '',
+ currentNodeCode: '',
+ currentNodeName: '',
+ totalNodes: '',
+ completedNodes: '',
+ progressRate: '',
+ estimatedDurationMinutes: '',
+ actualDurationMinutes: '',
+ startTime: '',
+ endTime: '',
+ resultCode: '',
+ resultMessage: '',
+ resultData: '',
+ retryTimes: '',
+ lastRetryTime: '',
+ extParams: '',
+ remark: '',
+ memo: '',
+ status: ''
+ }
+}
+
+export function getTaskInstancePaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function buildTaskInstanceSearchParams(params = {}) {
+ const result = {}
+
+ ;[
+ 'condition',
+ 'taskNo',
+ 'bizNo',
+ 'bizType',
+ 'templateCode',
+ 'sourceInfo',
+ 'targetInfo',
+ 'sourceCode',
+ 'targetCode',
+ 'plannedPath',
+ 'actualPath',
+ 'timeoutAt',
+ 'currentNodeCode',
+ 'currentNodeName',
+ 'startTime',
+ 'endTime',
+ 'resultCode',
+ 'resultMessage',
+ 'resultData',
+ 'lastRetryTime',
+ 'extParams',
+ 'remark',
+ 'memo'
+ ].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ ;['timeStart', 'timeEnd'].forEach((key) => {
+ if (params[key]) {
+ result[key] = params[key]
+ }
+ })
+
+ ;[
+ 'templateId',
+ 'templateVersion',
+ 'priority',
+ 'totalNodes',
+ 'completedNodes',
+ 'progressRate',
+ 'estimatedDurationMinutes',
+ 'actualDurationMinutes',
+ 'retryTimes',
+ 'status'
+ ].forEach((key) => {
+ const value = normalizeNumber(params[key])
+ if (value !== null) {
+ result[key] = value
+ }
+ })
+
+ return {
+ condition: '',
+ ...result
+ }
+}
+
+export function buildTaskInstancePageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTaskInstanceSearchParams(params)
+ }
+}
+
+export function normalizeTaskInstanceRow(record = {}) {
+ const statusMeta = STATUS_META[Number(record.status)] || STATUS_META[0]
+
+ return {
+ ...record,
+ id: record.id ?? '--',
+ taskNo: normalizeText(record.taskNo) || '--',
+ bizNo: normalizeText(record.bizNo) || '--',
+ bizType: normalizeText(record.bizType) || '--',
+ templateCode: normalizeText(record.templateCode) || '--',
+ sourceCode: normalizeText(record.sourceCode) || '--',
+ targetCode: normalizeText(record.targetCode) || '--',
+ currentNodeName: normalizeText(record.currentNodeName) || '--',
+ progressRate: record.progressRate ?? '--',
+ resultMessage: normalizeText(record.resultMessage) || '--',
+ startTimeText: normalizeDateTime(record['startTime$'] || record.startTimeText || record.startTime),
+ endTimeText: normalizeDateTime(record['endTime$'] || record.endTimeText || record.endTime),
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ memo: normalizeText(record.memo) || '--',
+ remark: normalizeText(record.remark) || '--'
+ }
+}
+
+export function getTaskInstanceReportColumns() {
+ return [
+ { prop: 'taskNo', label: '浠诲姟鍙�' },
+ { prop: 'bizNo', label: '涓氬姟鍗曞彿' },
+ { prop: 'bizType', label: '涓氬姟绫诲瀷' },
+ { prop: 'templateCode', label: '妯℃澘缂栫爜' },
+ { prop: 'sourceCode', label: '鏉ユ簮缂栫爜' },
+ { prop: 'targetCode', label: '鐩爣缂栫爜' },
+ { prop: 'currentNodeName', label: '褰撳墠鑺傜偣' },
+ { prop: 'progressRate', label: '鑺傜偣杩涘害' },
+ { prop: 'resultMessage', label: '缁撴灉淇℃伅' },
+ { prop: 'startTimeText', label: '寮�濮嬫椂闂�' },
+ { prop: 'endTimeText', label: '缁撴潫鏃堕棿' },
+ { prop: 'statusText', label: '鐘舵��' },
+ { prop: 'memo', label: '澶囨敞' }
+ ]
+}
+
+export function buildTaskInstancePrintRows(records = []) {
+ return (Array.isArray(records) ? records : []).map((record) => {
+ const row = normalizeTaskInstanceRow(record)
+ return {
+ taskNo: row.taskNo,
+ bizNo: row.bizNo,
+ bizType: row.bizType,
+ templateCode: row.templateCode,
+ sourceCode: row.sourceCode,
+ targetCode: row.targetCode,
+ currentNodeName: row.currentNodeName,
+ progressRate: row.progressRate,
+ resultMessage: row.resultMessage,
+ startTimeText: row.startTimeText,
+ endTimeText: row.endTimeText,
+ statusText: row.statusText,
+ memo: row.memo
+ }
+ })
+}
diff --git a/rsf-design/src/views/system/task-instance/taskInstanceTable.columns.js b/rsf-design/src/views/system/task-instance/taskInstanceTable.columns.js
new file mode 100644
index 0000000..8bfac20
--- /dev/null
+++ b/rsf-design/src/views/system/task-instance/taskInstanceTable.columns.js
@@ -0,0 +1,97 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createTaskInstanceTableColumns({ handleView } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'taskNo',
+ label: '浠诲姟鍙�',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'bizNo',
+ label: '涓氬姟鍗曞彿',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'bizType',
+ label: '涓氬姟绫诲瀷',
+ minWidth: 120,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'templateCode',
+ label: '妯℃澘缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'currentNodeName',
+ label: '褰撳墠鑺傜偣',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'progressRate',
+ label: '鑺傜偣杩涘害',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'resultMessage',
+ label: '缁撴灉淇℃伅',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'startTimeText',
+ label: '寮�濮嬫椂闂�',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'endTimeText',
+ label: '缁撴潫鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ align: 'center',
+ formatter: (row) =>
+ h(
+ ElTag,
+ {
+ type: row?.statusType || 'info',
+ effect: 'light'
+ },
+ () => row?.statusText || '--'
+ )
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 92,
+ align: 'center',
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:eye-line',
+ onClick: () => handleView?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/tenant/index.vue b/rsf-design/src/views/system/tenant/index.vue
new file mode 100644
index 0000000..2bbfe52
--- /dev/null
+++ b/rsf-design/src/views/system/tenant/index.vue
@@ -0,0 +1,283 @@
+<template>
+ <div class="tenant-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="false"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton v-auth="'add'" @click="showInitDialog" v-ripple>鍒濆鍖栫鎴�</ElButton>
+ <ElButton
+ v-auth="'delete'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ v-ripple
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <TenantInitDialog
+ v-model:visible="initDialogVisible"
+ :tenant-data="currentInitTenantData"
+ @submit="handleInitSubmit"
+ />
+
+ <TenantEditDialog
+ v-model:visible="editDialogVisible"
+ :tenant-data="currentEditTenantData"
+ @submit="handleEditSubmit"
+ />
+
+ <TenantDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail-data="detailData"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useAuth } from '@/hooks/core/useAuth'
+ import { useTable } from '@/hooks/core/useTable'
+ import {
+ fetchDeleteTenant,
+ fetchGetTenantDetail,
+ fetchInitTenant,
+ fetchTenantPage,
+ fetchUpdateTenant
+ } from '@/api/system-manage'
+ import TenantDetailDrawer from './modules/tenant-detail-drawer.vue'
+ import TenantEditDialog from './modules/tenant-edit-dialog.vue'
+ import TenantInitDialog from './modules/tenant-init-dialog.vue'
+ import { createTenantTableColumns } from './tenantTable.columns'
+ import {
+ buildTenantEditDialogModel,
+ buildTenantInitDialogModel,
+ buildTenantInitPayload,
+ buildTenantPageQueryParams,
+ buildTenantSearchParams,
+ buildTenantUpdatePayload,
+ createTenantSearchState,
+ getTenantPaginationKey,
+ getTenantStatusOptions,
+ normalizeTenantListRow
+ } from './tenantPage.helpers'
+
+ defineOptions({ name: 'Tenant' })
+
+ const { hasAuth } = useAuth()
+ const searchForm = ref(createTenantSearchState())
+ const selectedRows = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const initDialogVisible = ref(false)
+ const editDialogVisible = ref(false)
+ const currentInitTenantData = ref(buildTenantInitDialogModel())
+ const currentEditTenantData = ref(buildTenantEditDialogModel())
+ let handleDeleteAction = null
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ鎴峰悕绉版垨鏍囪瘑'
+ }
+ },
+ {
+ label: '绉熸埛鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ鎴峰悕绉�'
+ }
+ },
+ {
+ label: '绉熸埛鏍囪瘑',
+ key: 'flag',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ鎴锋爣璇�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: getTenantStatusOptions()
+ }
+ }
+ ])
+
+ async function openDetail(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ detailData.value = normalizeTenantListRow(await fetchGetTenantDetail(row.id))
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇绉熸埛璇︽儏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function openEditDialog(row) {
+ try {
+ currentEditTenantData.value = buildTenantEditDialogModel(await fetchGetTenantDetail(row.id))
+ editDialogVisible.value = true
+ } catch (error) {
+ ElMessage.error(error?.message || '鑾峰彇绉熸埛璇︽儏澶辫触')
+ }
+ }
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ refreshCreate,
+ refreshUpdate,
+ refreshRemove
+ } = useTable({
+ core: {
+ apiFn: fetchTenantPage,
+ apiParams: buildTenantPageQueryParams(searchForm.value),
+ paginationKey: getTenantPaginationKey(),
+ columnsFactory: () =>
+ createTenantTableColumns({
+ handleView: openDetail,
+ handleEdit: openEditDialog,
+ handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null
+ })
+ },
+ transform: {
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ return []
+ }
+ return records.map((item) => normalizeTenantListRow(item))
+ }
+ }
+ })
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function showInitDialog() {
+ currentInitTenantData.value = buildTenantInitDialogModel()
+ initDialogVisible.value = true
+ }
+
+ async function handleInitSubmit(formData) {
+ try {
+ await fetchInitTenant(buildTenantInitPayload(formData))
+ ElMessage.success('绉熸埛鍒濆鍖栨垚鍔�')
+ initDialogVisible.value = false
+ await refreshCreate?.()
+ } catch (error) {
+ ElMessage.error(error?.message || '绉熸埛鍒濆鍖栧け璐�')
+ }
+ }
+
+ async function handleEditSubmit(formData) {
+ try {
+ await fetchUpdateTenant(buildTenantUpdatePayload(formData))
+ ElMessage.success('淇敼鎴愬姛')
+ editDialogVisible.value = false
+ await refreshUpdate?.()
+ } catch (error) {
+ ElMessage.error(error?.message || '淇敼澶辫触')
+ }
+ }
+
+ async function handleDelete(record) {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄ょ鎴枫��${record?.name || record?.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteTenant(record.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await refreshRemove?.()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+ }
+ }
+ }
+ handleDeleteAction = handleDelete
+
+ async function handleBatchDelete() {
+ if (!selectedRows.value.length) return
+ const ids = selectedRows.value
+ .map((item) => item.id)
+ .filter((id) => id !== void 0 && id !== null)
+ if (!ids.length) return
+
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佹壒閲忓垹闄ら�変腑鐨� ${ids.length} 涓鎴峰悧锛焋, '鎵归噺鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchDeleteTenant(ids.join(','))
+ ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+ selectedRows.value = []
+ await refreshRemove?.()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error(error?.message || '鎵归噺鍒犻櫎澶辫触')
+ }
+ }
+ }
+
+ function handleSearch(params) {
+ replaceSearchParams(buildTenantSearchParams(params))
+ getData()
+ }
+
+ function handleReset() {
+ Object.assign(searchForm.value, createTenantSearchState())
+ resetSearchParams()
+ }
+</script>
diff --git a/rsf-design/src/views/system/tenant/modules/tenant-detail-drawer.vue b/rsf-design/src/views/system/tenant/modules/tenant-detail-drawer.vue
new file mode 100644
index 0000000..c8f0247
--- /dev/null
+++ b/rsf-design/src/views/system/tenant/modules/tenant-detail-drawer.vue
@@ -0,0 +1,39 @@
+<template>
+ <ElDrawer
+ :model-value="visible"
+ title="绉熸埛璇︽儏"
+ size="560px"
+ @update:model-value="handleVisibleChange"
+ >
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <ElDescriptions :column="1" border>
+ <ElDescriptionsItem label="绉熸埛鍚嶇О">{{ displayData.name || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绉熸埛鏍囪瘑">{{ displayData.flag || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绠$悊鍛�">{{ displayData.rootLabel || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="displayData.statusType" effect="light">{{ displayData.statusText || '--' }}</ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </ElSkeleton>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { normalizeTenantListRow } from '../tenantPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ loading: { type: Boolean, default: false },
+ detailData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+ const displayData = computed(() => normalizeTenantListRow(props.detailData))
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/system/tenant/modules/tenant-edit-dialog.vue b/rsf-design/src/views/system/tenant/modules/tenant-edit-dialog.vue
new file mode 100644
index 0000000..048c2f7
--- /dev/null
+++ b/rsf-design/src/views/system/tenant/modules/tenant-edit-dialog.vue
@@ -0,0 +1,149 @@
+<template>
+ <ElDialog
+ title="缂栬緫绉熸埛"
+ :model-value="visible"
+ width="760px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildTenantEditDialogModel,
+ createTenantEditFormState,
+ getTenantFlagPattern,
+ getTenantStatusOptions
+ } from '../tenantPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ tenantData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createTenantEditFormState())
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ョ鎴峰悕绉�', trigger: 'blur' }],
+ flag: [
+ { required: true, message: '璇疯緭鍏ョ鎴锋爣璇�', trigger: 'blur' },
+ {
+ pattern: getTenantFlagPattern(),
+ message: '绉熸埛鏍囪瘑浠呮敮鎸� 3-20 浣嶈嫳鏂囧瓧姣�',
+ trigger: 'blur'
+ }
+ ]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '绉熸埛鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ鎴峰悕绉�'
+ }
+ },
+ {
+ label: '绉熸埛鏍囪瘑',
+ key: 'flag',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ鎴锋爣璇�'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: getTenantStatusOptions(),
+ placeholder: '璇烽�夋嫨鐘舵��'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createTenantEditFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildTenantEditDialogModel(props.tenantData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.tenantData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/tenant/modules/tenant-init-dialog.vue b/rsf-design/src/views/system/tenant/modules/tenant-init-dialog.vue
new file mode 100644
index 0000000..8909609
--- /dev/null
+++ b/rsf-design/src/views/system/tenant/modules/tenant-init-dialog.vue
@@ -0,0 +1,219 @@
+<template>
+ <ElDialog
+ title="鍒濆鍖栫鎴�"
+ :model-value="visible"
+ width="860px"
+ align-center
+ @update:model-value="handleCancel"
+ @closed="handleClosed"
+ >
+ <div class="mb-5 rounded-xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-4 py-3">
+ <p class="text-sm text-[var(--art-gray-700)]">鍒涘缓绉熸埛鏃朵細鍚屾椂鍒濆鍖栫鎴风鐞嗗憳璐﹀彿銆�</p>
+ </div>
+
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="12"
+ :gutter="20"
+ label-width="110px"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾鍒濆鍖�</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+ import {
+ buildTenantInitDialogModel,
+ createTenantInitFormState,
+ getTenantFlagPattern,
+ getTenantPasswordPattern,
+ getTenantUsernamePattern
+ } from '../tenantPage.helpers'
+
+ const props = defineProps({
+ visible: { type: Boolean, default: false },
+ tenantData: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const form = reactive(createTenantInitFormState())
+
+ const rules = computed(() => ({
+ name: [{ required: true, message: '璇疯緭鍏ョ鎴峰悕绉�', trigger: 'blur' }],
+ flag: [
+ { required: true, message: '璇疯緭鍏ョ鎴锋爣璇�', trigger: 'blur' },
+ {
+ pattern: getTenantFlagPattern(),
+ message: '绉熸埛鏍囪瘑浠呮敮鎸� 3-20 浣嶈嫳鏂囧瓧姣�',
+ trigger: 'blur'
+ }
+ ],
+ username: [
+ { required: true, message: '璇疯緭鍏ョ鐞嗗憳璐﹀彿', trigger: 'blur' },
+ {
+ pattern: getTenantUsernamePattern(),
+ message: '绠$悊鍛樿处鍙蜂粎鏀寔 3-20 浣嶅瓧姣嶆垨鏁板瓧',
+ trigger: 'blur'
+ }
+ ],
+ email: [
+ {
+ type: 'email',
+ message: '璇疯緭鍏ユ纭殑閭鍦板潃',
+ trigger: 'blur'
+ }
+ ],
+ password: [
+ { required: true, message: '璇疯緭鍏ュ垵濮嬪寲瀵嗙爜', trigger: 'blur' },
+ {
+ pattern: getTenantPasswordPattern(),
+ message: '瀵嗙爜闇�涓� 6-13 浣嶄笖鍚屾椂鍖呭惈瀛楁瘝鍜屾暟瀛�',
+ trigger: 'blur'
+ }
+ ],
+ confirmPassword: [
+ { required: true, message: '璇峰啀娆¤緭鍏ュ垵濮嬪寲瀵嗙爜', trigger: 'blur' },
+ {
+ validator: (_rule, value, callback) => {
+ if (value !== form.password) {
+ callback(new Error('涓ゆ杈撳叆鐨勫瘑鐮佷笉涓�鑷�'))
+ return
+ }
+ callback()
+ },
+ trigger: 'blur'
+ }
+ ]
+ }))
+
+ const formItems = computed(() => [
+ {
+ label: '绉熸埛鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ鎴峰悕绉�'
+ }
+ },
+ {
+ label: '绉熸埛鏍囪瘑',
+ key: 'flag',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏� 3-20 浣嶈嫳鏂囨爣璇�'
+ }
+ },
+ {
+ label: '绠$悊鍛樿处鍙�',
+ key: 'username',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ鐞嗗憳璐﹀彿'
+ }
+ },
+ {
+ label: '绠$悊鍛橀偖绠�',
+ key: 'email',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ鐞嗗憳閭'
+ }
+ },
+ {
+ label: '鍒濆鍖栧瘑鐮�',
+ key: 'password',
+ type: 'input',
+ props: {
+ type: 'password',
+ showPassword: true,
+ placeholder: '璇疯緭鍏ュ垵濮嬪寲瀵嗙爜'
+ }
+ },
+ {
+ label: '纭瀵嗙爜',
+ key: 'confirmPassword',
+ type: 'input',
+ props: {
+ type: 'password',
+ showPassword: true,
+ placeholder: '璇峰啀娆¤緭鍏ュ垵濮嬪寲瀵嗙爜'
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ span: 24,
+ props: {
+ type: 'textarea',
+ rows: 3,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ function resetForm() {
+ Object.assign(form, createTenantInitFormState())
+ formRef.value?.clearValidate?.()
+ }
+
+ function loadFormData() {
+ Object.assign(form, buildTenantInitDialogModel(props.tenantData))
+ }
+
+ async function handleSubmit() {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ } catch {
+ return
+ }
+ }
+
+ function handleCancel() {
+ emit('update:visible', false)
+ }
+
+ function handleClosed() {
+ resetForm()
+ }
+
+ watch(
+ () => props.visible,
+ (visible) => {
+ if (visible) {
+ loadFormData()
+ nextTick(() => formRef.value?.clearValidate?.())
+ }
+ },
+ { immediate: true }
+ )
+
+ watch(
+ () => props.tenantData,
+ () => {
+ if (props.visible) {
+ loadFormData()
+ }
+ },
+ { deep: true }
+ )
+</script>
diff --git a/rsf-design/src/views/system/tenant/tenantPage.helpers.js b/rsf-design/src/views/system/tenant/tenantPage.helpers.js
new file mode 100644
index 0000000..8f57ea0
--- /dev/null
+++ b/rsf-design/src/views/system/tenant/tenantPage.helpers.js
@@ -0,0 +1,148 @@
+const STATUS_OPTIONS = [
+ { label: '姝e父', value: 1 },
+ { label: '绂佺敤', value: 0 }
+]
+
+const TENANT_FLAG_PATTERN = /^[A-Za-z]{3,20}$/
+const TENANT_USERNAME_PATTERN = /^[A-Za-z0-9]{3,20}$/
+const TENANT_PASSWORD_PATTERN = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d.]{6,13}$/
+
+export function createTenantSearchState() {
+ return {
+ condition: '',
+ name: '',
+ flag: '',
+ status: ''
+ }
+}
+
+export function createTenantInitFormState() {
+ return {
+ name: '',
+ flag: '',
+ username: '',
+ email: '',
+ password: '',
+ confirmPassword: '',
+ memo: ''
+ }
+}
+
+export function createTenantEditFormState() {
+ return {
+ id: null,
+ name: '',
+ flag: '',
+ status: 1,
+ memo: ''
+ }
+}
+
+export function getTenantPaginationKey() {
+ return {
+ current: 'current',
+ size: 'pageSize'
+ }
+}
+
+export function getTenantStatusOptions() {
+ return STATUS_OPTIONS
+}
+
+export function getTenantStatusMeta(status) {
+ return Number(status) === 1
+ ? { text: '姝e父', type: 'success', bool: true }
+ : { text: '绂佺敤', type: 'danger', bool: false }
+}
+
+export function getTenantFlagPattern() {
+ return TENANT_FLAG_PATTERN
+}
+
+export function getTenantUsernamePattern() {
+ return TENANT_USERNAME_PATTERN
+}
+
+export function getTenantPasswordPattern() {
+ return TENANT_PASSWORD_PATTERN
+}
+
+export function buildTenantSearchParams(params = {}) {
+ return {
+ condition: String(params.condition || '').trim(),
+ name: String(params.name || '').trim(),
+ flag: String(params.flag || '').trim(),
+ ...(params.status !== '' && params.status !== null && params.status !== undefined
+ ? { status: Number(params.status) }
+ : {})
+ }
+}
+
+export function buildTenantPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildTenantSearchParams(params)
+ }
+}
+
+export function buildTenantInitDialogModel(record = {}) {
+ return {
+ ...createTenantInitFormState(),
+ name: record.name || '',
+ flag: record.flag || '',
+ username: record.username || '',
+ email: record.email || '',
+ password: '',
+ confirmPassword: '',
+ memo: record.memo || ''
+ }
+}
+
+export function buildTenantEditDialogModel(record = {}) {
+ return {
+ ...createTenantEditFormState(),
+ ...(record.id ? { id: Number(record.id) } : {}),
+ name: record.name || '',
+ flag: record.flag || '',
+ status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
+ memo: record.memo || ''
+ }
+}
+
+export function buildTenantInitPayload(formData = {}) {
+ return {
+ name: String(formData.name || '').trim(),
+ flag: String(formData.flag || '').trim(),
+ username: String(formData.username || '').trim(),
+ ...(String(formData.email || '').trim() ? { email: String(formData.email || '').trim() } : {}),
+ password: String(formData.password || '').trim(),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function buildTenantUpdatePayload(formData = {}) {
+ return {
+ ...(formData.id ? { id: Number(formData.id) } : {}),
+ name: String(formData.name || '').trim(),
+ flag: String(formData.flag || '').trim(),
+ status: Number(formData.status ?? 1),
+ memo: String(formData.memo || '').trim()
+ }
+}
+
+export function normalizeTenantListRow(record = {}) {
+ const statusMeta = getTenantStatusMeta(record.status)
+ return {
+ ...record,
+ name: record.name || '',
+ flag: record.flag || '',
+ memo: record.memo || '',
+ rootLabel: record['root$'] || record.root || '',
+ statusText: record['status$'] || statusMeta.text,
+ statusType: statusMeta.type,
+ statusBool: record.statusBool ?? statusMeta.bool,
+ updateTimeText: record['updateTime$'] || record.updateTime || '',
+ createTimeText: record['createTime$'] || record.createTime || ''
+ }
+}
diff --git a/rsf-design/src/views/system/tenant/tenantTable.columns.js b/rsf-design/src/views/system/tenant/tenantTable.columns.js
new file mode 100644
index 0000000..2c1fcd1
--- /dev/null
+++ b/rsf-design/src/views/system/tenant/tenantTable.columns.js
@@ -0,0 +1,80 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createTenantTableColumns({ handleView, handleEdit, handleDelete }) {
+ return [
+ {
+ prop: 'name',
+ label: '绉熸埛鍚嶇О',
+ minWidth: 180,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'flag',
+ label: '绉熸埛鏍囪瘑',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'rootLabel',
+ label: '绠$悊鍛�',
+ minWidth: 120,
+ formatter: (row) => row.rootLabel || '-'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) => h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || '-')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.updateTimeText || '-'
+ },
+ {
+ prop: 'createTimeText',
+ label: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ formatter: (row) => row.createTimeText || '-'
+ },
+ {
+ prop: 'memo',
+ label: '澶囨敞',
+ minWidth: 180,
+ showOverflowTooltip: true,
+ formatter: (row) => row.memo || '-'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: handleDelete ? 160 : 120,
+ align: 'right',
+ formatter: (row) => {
+ const buttons = [
+ h(ArtButtonTable, {
+ type: 'view',
+ onClick: () => handleView(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ })
+ ]
+
+ if (handleDelete) {
+ buttons.push(
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ )
+ }
+
+ return h('div', { class: 'flex justify-end' }, buttons)
+ }
+ }
+ ]
+}
diff --git a/rsf-design/src/views/system/user/index.vue b/rsf-design/src/views/system/user/index.vue
index 5adc86b..8800cf1 100644
--- a/rsf-design/src/views/system/user/index.vue
+++ b/rsf-design/src/views/system/user/index.vue
@@ -47,6 +47,7 @@
<script setup>
import request from '@/utils/http'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
import {
fetchDeleteUser,
fetchGetDeptTree,
@@ -247,7 +248,19 @@
const loadLookups = async () => {
try {
- const [roles, depts] = await Promise.all([fetchGetRoleOptions({}), fetchGetDeptTree({})])
+ const lookupPayload = await guardRequestWithMessage(
+ Promise.all([fetchGetRoleOptions({}), fetchGetDeptTree({})]),
+ null,
+ {
+ timeoutMessage: '鐢ㄦ埛椤靛瓧鍏稿姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�'
+ }
+ )
+ if (!lookupPayload) {
+ roleOptions.value = []
+ deptTreeOptions.value = []
+ return
+ }
+ const [roles, depts] = lookupPayload
roleOptions.value = normalizeRoleOptions(roles)
deptTreeOptions.value = normalizeDeptTreeOptions(depts)
} catch (error) {
diff --git a/rsf-design/src/views/work/check-out-bound/checkOutBoundPage.helpers.js b/rsf-design/src/views/work/check-out-bound/checkOutBoundPage.helpers.js
new file mode 100644
index 0000000..7865046
--- /dev/null
+++ b/rsf-design/src/views/work/check-out-bound/checkOutBoundPage.helpers.js
@@ -0,0 +1,178 @@
+const CHECK_OUT_BOUND_REPORT_TITLE = '鐩樼偣鍑哄簱浠诲姟鐢熸垚'
+
+function normalizeText(value) {
+ return typeof value === 'string' ? value.trim() : value
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+function filterParams(params = {}, ignoredKeys = []) {
+ return Object.fromEntries(
+ Object.entries(params)
+ .filter(([key, value]) => {
+ if (ignoredKeys.includes(key)) return false
+ if (value === undefined || value === null) return false
+ if (typeof value === 'string' && value.trim() === '') return false
+ return true
+ })
+ .map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value])
+ )
+}
+
+function getCheckOutBoundStatusMeta(status) {
+ const normalized = Number(status)
+ if (normalized === 1) {
+ return { text: '姝e父', type: 'success' }
+ }
+ if (normalized === 0) {
+ return { text: '鍐荤粨', type: 'danger' }
+ }
+ return { text: '--', type: 'info' }
+}
+
+export function createCheckOutBoundSearchState() {
+ return {
+ condition: '',
+ locCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ trackCode: ''
+ }
+}
+
+export function buildCheckOutBoundSearchParams(params = {}) {
+ return filterParams(params, ['current', 'pageSize', 'size'])
+}
+
+export function buildCheckOutBoundPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...filterParams(params, ['current', 'pageSize', 'size'])
+ }
+}
+
+function normalizeTaskItemRow(row = {}) {
+ const {
+ statusText,
+ statusTagType,
+ createTimeText,
+ updateTimeText,
+ actionList,
+ operation,
+ ...rest
+ } = row
+ return {
+ ...rest
+ }
+}
+
+export function buildCheckOutBoundTaskPayload({ siteNo = '', items = [], memo = '' } = {}) {
+ const payload = {
+ siteNo: normalizeText(siteNo),
+ items: Array.isArray(items) ? items.map((item) => normalizeTaskItemRow(item)) : []
+ }
+
+ const normalizedMemo = normalizeText(memo)
+ if (normalizedMemo) {
+ payload.memo = normalizedMemo
+ }
+
+ return payload
+}
+
+export function normalizeCheckOutBoundRow(row = {}) {
+ const statusMeta = getCheckOutBoundStatusMeta(row.status ?? row.status$)
+ return {
+ ...row,
+ locCode: normalizeText(row.locCode || row.locCode$ || row.locId || '--') || '--',
+ matnrCode: normalizeText(row.matnrCode || row.matnrCode$ || '--') || '--',
+ maktx: normalizeText(row.maktx || row.maktx$ || '--') || '--',
+ batch: normalizeText(row.batch || row.batch$ || '--') || '--',
+ splrBatch: normalizeText(row.splrBatch || row.splrBatch$ || '--') || '--',
+ unit: normalizeText(row.unit || row.unit$ || '--') || '--',
+ trackCode: normalizeText(row.trackCode || row.trackCode$ || '--') || '--',
+ siteNo: normalizeText(row.siteNo || row.siteNo$ || '--') || '--',
+ memo: normalizeText(row.memo || row.memo$ || '--') || '--',
+ anfme: normalizeNumber(row.anfme),
+ workQty: normalizeNumber(row.workQty),
+ qty: normalizeNumber(row.qty),
+ statusText: row.statusText || row.status$ || statusMeta.text,
+ statusTagType: row.statusTagType || statusMeta.type,
+ createTimeText: row.createTimeText || row.createTime$ || row.createTime || '--',
+ updateTimeText: row.updateTimeText || row.updateTime$ || row.updateTime || '--'
+ }
+}
+
+export function resolveCheckOutBoundSiteOptions(source = []) {
+ const records = Array.isArray(source)
+ ? source
+ : Array.isArray(source?.records)
+ ? source.records
+ : Array.isArray(source?.list)
+ ? source.list
+ : Array.isArray(source?.data)
+ ? source.data
+ : Array.isArray(source?.data?.records)
+ ? source.data.records
+ : Array.isArray(source?.data?.list)
+ ? source.data.list
+ : Array.isArray(source?.data?.data)
+ ? source.data.data
+ : []
+ const seen = new Set()
+
+ return records
+ .map((item) => {
+ if (typeof item === 'string' || typeof item === 'number') {
+ const value = String(item).trim()
+ return value ? { value, label: value } : null
+ }
+
+ const value = normalizeText(item?.siteNo ?? item?.site ?? item?.code ?? item?.value ?? item?.id)
+ if (!value) return null
+
+ return {
+ value,
+ label: normalizeText(item?.name ?? item?.siteName ?? item?.label ?? item?.code ?? value)
+ }
+ })
+ .filter(Boolean)
+ .filter((item) => {
+ if (seen.has(item.value)) return false
+ seen.add(item.value)
+ return true
+ })
+}
+
+export function getCheckOutBoundActionList() {
+ return [
+ {
+ key: 'view',
+ label: '鏌ョ湅鏄庣粏',
+ icon: 'ri:eye-line'
+ }
+ ]
+}
+
+export function buildCheckOutBoundReportMeta(rows = []) {
+ return {
+ reportTitle: CHECK_OUT_BOUND_REPORT_TITLE,
+ reportDate: new Date().toLocaleDateString('zh-CN'),
+ printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
+ count: Array.isArray(rows) ? rows.length : 0
+ }
+}
+
+export function buildCheckOutBoundPrintRows(records = []) {
+ return Array.isArray(records) ? records.map((item) => normalizeCheckOutBoundRow(item)) : []
+}
+
+export { CHECK_OUT_BOUND_REPORT_TITLE, getCheckOutBoundStatusMeta }
diff --git a/rsf-design/src/views/work/check-out-bound/checkOutBoundTable.columns.js b/rsf-design/src/views/work/check-out-bound/checkOutBoundTable.columns.js
new file mode 100644
index 0000000..f83c917
--- /dev/null
+++ b/rsf-design/src/views/work/check-out-bound/checkOutBoundTable.columns.js
@@ -0,0 +1,94 @@
+import { h } from 'vue'
+import { ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { getCheckOutBoundActionList } from './checkOutBoundPage.helpers'
+
+export function createCheckOutBoundTableColumns({ handleActionClick } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 140,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'batch',
+ label: '搴撳瓨鎵规',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'anfme',
+ label: '鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '瀹屾垚鏁伴噺',
+ width: 100,
+ align: 'right'
+ },
+ {
+ prop: 'trackCode',
+ label: '杩借釜鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) =>
+ h(ElTag, { type: row.statusTagType || 'info', effect: 'light' }, () => row.statusText || '--')
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 110,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: getCheckOutBoundActionList(row),
+ onClick: (item) => handleActionClick?.(item, row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/src/views/work/check-out-bound/index.vue b/rsf-design/src/views/work/check-out-bound/index.vue
new file mode 100644
index 0000000..4e46fe1
--- /dev/null
+++ b/rsf-design/src/views/work/check-out-bound/index.vue
@@ -0,0 +1,310 @@
+<template>
+ <div class="check-out-bound-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card mb-4">
+ <div class="flex flex-wrap items-center gap-3">
+ <div class="text-sm text-[var(--art-text-gray-600)]">
+ 宸查�夋嫨 <span class="font-medium text-[var(--art-text-gray-800)]">{{ selectedRows.length }}</span> 鏉″簱浣嶇墿鏂�
+ </div>
+ <ElSelect
+ v-model="siteNo"
+ class="min-w-56"
+ filterable
+ clearable
+ placeholder="璇烽�夋嫨绔欑偣"
+ :loading="siteLoading"
+ :disabled="siteLoading"
+ >
+ <ElOption
+ v-for="item in siteOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </ElSelect>
+ <ElButton
+ type="primary"
+ :loading="generateLoading"
+ :disabled="!canGenerateTask"
+ @click="handleGenerateTask"
+ >
+ 鐢熸垚鐩樼偣鍑哄簱浠诲姟
+ </ElButton>
+ <ElButton :disabled="selectedRows.length === 0" @click="clearSelection">娓呯┖閫夋嫨</ElButton>
+ </div>
+ </ElCard>
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <CheckOutBoundDetailDrawer
+ v-model:visible="detailDrawerVisible"
+ :loading="detailLoading"
+ :detail="detailData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref, watch } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ buildCheckOutBoundPageQueryParams,
+ buildCheckOutBoundTaskPayload,
+ createCheckOutBoundSearchState,
+ normalizeCheckOutBoundRow,
+ resolveCheckOutBoundSiteOptions
+ } from './checkOutBoundPage.helpers'
+ import {
+ fetchCheckOutBoundPage,
+ fetchCheckOutBoundSites,
+ fetchGenerateCheckOutBoundTask,
+ fetchGetCheckOutBoundDetail
+ } from '@/api/check-out-bound'
+ import CheckOutBoundDetailDrawer from './modules/check-out-bound-detail-drawer.vue'
+ import { createCheckOutBoundTableColumns } from './checkOutBoundTable.columns'
+
+ defineOptions({ name: 'CheckOutBound' })
+
+ const searchForm = ref(createCheckOutBoundSearchState())
+ const selectedRows = ref([])
+ const siteOptions = ref([])
+ const siteLoading = ref(false)
+ const siteNo = ref('')
+ const memo = ref('')
+ const detailDrawerVisible = ref(false)
+ const detailLoading = ref(false)
+ const detailData = ref({})
+ const generateLoading = ref(false)
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�/鐗╂枡缂栫爜/鐗╂枡鍚嶇О/鎵规'
+ }
+ },
+ {
+ label: '搴撲綅缂栫爜',
+ key: 'locCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '搴撳瓨鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱瀛樻壒娆�'
+ }
+ },
+ {
+ label: '杩借釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ拷韪爜'
+ }
+ }
+ ])
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange: handleTableSizeChange,
+ handleCurrentChange: handleTableCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchCheckOutBoundPage,
+ apiParams: buildCheckOutBoundPageQueryParams(searchForm.value),
+ columnsFactory: () =>
+ createCheckOutBoundTableColumns({
+ handleActionClick
+ })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeCheckOutBoundRow(item)) : [])
+ }
+ })
+
+ const canGenerateTask = computed(() => selectedRows.value.length > 0 && Boolean(siteNo.value) && !generateLoading.value)
+
+ watch(
+ data,
+ () => {
+ selectedRows.value = []
+ },
+ { deep: false }
+ )
+
+ onMounted(loadSiteOptions)
+
+ async function loadSiteOptions() {
+ siteLoading.value = true
+ try {
+ const response = await guardRequestWithMessage(fetchCheckOutBoundSites(), [], {
+ timeoutMessage: '绔欑偣閫夐」鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ siteOptions.value = resolveCheckOutBoundSiteOptions(response)
+ if (siteOptions.value.length > 0 && !siteOptions.value.some((item) => item.value === siteNo.value)) {
+ siteNo.value = ''
+ }
+ } catch (error) {
+ siteOptions.value = []
+ ElMessage.error(error?.message || '鑾峰彇绔欑偣閫夐」澶辫触')
+ } finally {
+ siteLoading.value = false
+ }
+ }
+
+ async function openDetailDrawer(row) {
+ detailDrawerVisible.value = true
+ detailLoading.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetCheckOutBoundDetail(row.id), {}, {
+ timeoutMessage: '鐩樼偣鍑哄簱鏄庣粏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeCheckOutBoundRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇鐩樼偣鍑哄簱鏄庣粏澶辫触')
+ } finally {
+ detailLoading.value = false
+ }
+ }
+
+ async function handleActionClick(action, row) {
+ if (action?.disabled) return
+ if (action.key === 'view') {
+ await openDetailDrawer(row)
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function clearSelection() {
+ selectedRows.value = []
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ selectedRows.value = []
+ replaceSearchParams(buildCheckOutBoundPageQueryParams(searchForm.value))
+ refreshData()
+ }
+
+ function handleReset() {
+ searchForm.value = createCheckOutBoundSearchState()
+ selectedRows.value = []
+ siteNo.value = ''
+ memo.value = ''
+ resetSearchParams()
+ }
+
+ function handleSizeChange(size) {
+ selectedRows.value = []
+ handleTableSizeChange(size)
+ }
+
+ function handleCurrentChange(current) {
+ selectedRows.value = []
+ handleTableCurrentChange(current)
+ }
+
+ async function handleGenerateTask() {
+ if (!siteNo.value) {
+ ElMessage.warning('璇烽�夋嫨绔欑偣')
+ return
+ }
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning('璇烽�夋嫨搴撲綅鐗╂枡')
+ return
+ }
+
+ try {
+ await ElMessageBox.confirm(
+ `纭畾鍩轰簬褰撳墠鎵�閫� ${selectedRows.value.length} 鏉″簱浣嶇墿鏂欑敓鎴愮洏鐐瑰嚭搴撲换鍔″悧锛焋,
+ '鐢熸垚纭',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ )
+
+ generateLoading.value = true
+ const payload = buildCheckOutBoundTaskPayload({
+ siteNo: siteNo.value,
+ memo: memo.value,
+ items: selectedRows.value
+ })
+ await fetchGenerateCheckOutBoundTask(payload)
+ ElMessage.success('浠诲姟鐢熸垚鎴愬姛')
+ selectedRows.value = []
+ refreshData()
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') return
+ ElMessage.error(error?.message || '浠诲姟鐢熸垚澶辫触')
+ } finally {
+ generateLoading.value = false
+ }
+ }
+</script>
diff --git a/rsf-design/src/views/work/check-out-bound/modules/check-out-bound-detail-drawer.vue b/rsf-design/src/views/work/check-out-bound/modules/check-out-bound-detail-drawer.vue
new file mode 100644
index 0000000..0a01550
--- /dev/null
+++ b/rsf-design/src/views/work/check-out-bound/modules/check-out-bound-detail-drawer.vue
@@ -0,0 +1,65 @@
+<template>
+ <ElDrawer
+ v-model="visibleProxy"
+ title="鐩樼偣鍑哄簱鏄庣粏"
+ size="760px"
+ destroy-on-close
+ append-to-body
+ >
+ <ElScrollbar class="h-full">
+ <ElSkeleton :loading="loading" animated :rows="10">
+ <div class="space-y-4">
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="搴撳瓨鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹屾垚鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="杩借釜鐮�">{{ detail.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣">{{ detail.siteNo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">
+ <ElTag :type="detail.statusTagType || 'info'" effect="light">
+ {{ detail.statusText || '--' }}
+ </ElTag>
+ </ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElSkeleton>
+ </ElScrollbar>
+ </ElDrawer>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+
+ defineOptions({ name: 'CheckOutBoundDetailDrawer' })
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ default: false
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ detail: {
+ type: Object,
+ default: () => ({})
+ }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ const visibleProxy = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+</script>
diff --git a/rsf-design/src/views/work/out-bound/index.vue b/rsf-design/src/views/work/out-bound/index.vue
new file mode 100644
index 0000000..9b92b07
--- /dev/null
+++ b/rsf-design/src/views/work/out-bound/index.vue
@@ -0,0 +1,368 @@
+<template>
+ <div class="out-bound-page art-full-height">
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ :showExpand="true"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <span class="text-sm text-[var(--art-text-secondary)]">宸查�夊簱瀛� {{ selectedBrowseRows.length }} 鏉�</span>
+ <ElButton type="primary" :disabled="selectedBrowseRows.length === 0" @click="handleCollectSelectedRows">
+ 鍔犲叆浠诲姟绡�
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+
+ <ElCard class="art-table-card">
+ <template #header>
+ <div class="flex flex-wrap items-center justify-between gap-4">
+ <div>
+ <div class="text-base font-medium text-[var(--art-text-primary)]">浠诲姟绡�</div>
+ <div class="text-xs text-[var(--art-text-secondary)]">
+ {{ basketSummaryText }}
+ </div>
+ </div>
+
+ <ElSpace wrap>
+ <ElSelect
+ v-model="taskForm.siteNo"
+ filterable
+ clearable
+ placeholder="璇烽�夋嫨鍑哄簱绔欑偣"
+ style="min-width: 220px"
+ >
+ <ElOption v-for="option in siteOptions" :key="option.value" :label="option.label" :value="option.value" />
+ </ElSelect>
+ <ElInput v-model="taskForm.memo" clearable placeholder="璇疯緭鍏ヤ换鍔″娉�" style="min-width: 220px" />
+ <ElButton type="primary" :disabled="basketRows.length === 0" :loading="generating" @click="handleGenerateTask">
+ 鐢熸垚浠诲姟
+ </ElButton>
+ <ElButton :disabled="basketRows.length === 0" @click="handleClearBasket">娓呯┖浠诲姟绡�</ElButton>
+ </ElSpace>
+ </div>
+ </template>
+
+ <ArtTable :loading="false" :data="basketRows" :columns="basketColumns" :pagination="basketPagination" />
+ </ElCard>
+
+ <OutBoundDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
+ </div>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from 'vue'
+ import { ElMessage, ElMessageBox } from 'element-plus'
+ import { useTable } from '@/hooks/core/useTable'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+ import {
+ fetchGenerateOutboundTask,
+ fetchGetOutboundInventoryDetail,
+ fetchOutboundInventoryPage,
+ fetchOutboundSiteList,
+ normalizeOutboundTaskPayload
+ } from '@/api/out-bound'
+ import OutBoundDetailDrawer from './modules/out-bound-detail-drawer.vue'
+ import { createOutBoundInventoryTableColumns, createOutBoundBasketTableColumns } from './outBoundTable.columns'
+ import {
+ buildOutBoundGenerateTaskPayload,
+ buildOutBoundPageQueryParams,
+ createOutBoundSearchState,
+ createOutBoundTaskState,
+ getOutBoundValidationMessage,
+ mergeOutBoundBasketRows,
+ normalizeOutBoundBasketRows,
+ normalizeOutBoundRow,
+ normalizeOutBoundSiteOptions
+ } from './outBoundPage.helpers'
+
+ defineOptions({ name: 'OutBound' })
+
+ const searchForm = ref(createOutBoundSearchState())
+ const taskForm = ref(createOutBoundTaskState())
+ const selectedBrowseRows = ref([])
+ const basketRows = ref([])
+ const siteOptions = ref([])
+ const detailDrawerVisible = ref(false)
+ const detailData = ref({})
+ const generating = ref(false)
+
+ const searchItems = computed(() => [
+ {
+ label: '鍏抽敭瀛�',
+ key: 'condition',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣�/鐗╂枡/鎵规鍏抽敭瀛�'
+ }
+ },
+ {
+ label: '搴撲綅缂栫爜',
+ key: 'locCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ簱浣嶇紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡缂栫爜',
+ key: 'matnrCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�'
+ }
+ },
+ {
+ label: '鐗╂枡鍚嶇О',
+ key: 'maktx',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欏悕绉�'
+ }
+ },
+ {
+ label: '鎵规',
+ key: 'batch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ユ壒娆�'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗘壒娆�',
+ key: 'splrBatch',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヤ緵搴斿晢鎵规'
+ }
+ },
+ {
+ label: '杩借釜鐮�',
+ key: 'trackCode',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ヨ拷韪爜'
+ }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ clearable: true,
+ options: [
+ { label: '姝e父', value: 1 },
+ { label: '鍐荤粨', value: 0 }
+ ]
+ }
+ },
+ {
+ label: '澶囨敞',
+ key: 'memo',
+ type: 'input',
+ props: {
+ clearable: true,
+ placeholder: '璇疯緭鍏ュ娉�'
+ }
+ }
+ ])
+
+ const basketPagination = computed(() => ({
+ current: 1,
+ size: Math.max(basketRows.value.length, 1),
+ total: basketRows.value.length
+ }))
+
+ const basketSummaryText = computed(() => {
+ const totalQty = basketRows.value.reduce((sum, row) => sum + Number(row.outQty || 0), 0)
+ return `宸查�� ${basketRows.value.length} 鏉★紝鍚堣鍑哄簱鏁伴噺 ${totalQty}`
+ })
+
+ function buildBasketRowKey(row = {}) {
+ return row.id ?? `${row.locId || ''}-${row.matnrCode || ''}-${row.batch || ''}-${row.trackCode || ''}`
+ }
+
+ async function openDetailDrawer(row) {
+ detailDrawerVisible.value = true
+ try {
+ const detail = await guardRequestWithMessage(fetchGetOutboundInventoryDetail(row.id), {}, {
+ timeoutMessage: '搴撳瓨鏄庣粏璇︽儏鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ detailData.value = normalizeOutBoundRow(detail)
+ } catch (error) {
+ detailDrawerVisible.value = false
+ detailData.value = {}
+ ElMessage.error(error?.message || '鑾峰彇搴撳瓨鏄庣粏璇︽儏澶辫触')
+ }
+ }
+
+ function handleSelectionChange(rows) {
+ selectedBrowseRows.value = Array.isArray(rows) ? rows : []
+ }
+
+ function handleCollectSelectedRows(rows = selectedBrowseRows.value) {
+ if (!Array.isArray(rows) || rows.length === 0) {
+ ElMessage.warning('璇峰厛閫夋嫨搴撳瓨鏄庣粏')
+ return
+ }
+ basketRows.value = mergeOutBoundBasketRows(basketRows.value, rows)
+ selectedBrowseRows.value = []
+ ElMessage.success(`宸插姞鍏� ${rows.length} 鏉″簱瀛樻槑缁哷)
+ }
+
+ function handleAddToBasket(rows) {
+ basketRows.value = mergeOutBoundBasketRows(basketRows.value, rows)
+ }
+
+ function handleRemoveBasketRow(row) {
+ const key = buildBasketRowKey(row)
+ basketRows.value = basketRows.value.filter((item) => buildBasketRowKey(item) !== key)
+ }
+
+ function handleOutQtyChange(row, value) {
+ const key = buildBasketRowKey(row)
+ basketRows.value = basketRows.value.map((item) => {
+ if (buildBasketRowKey(item) !== key) {
+ return item
+ }
+ const numericValue = Number(value ?? 0)
+ return {
+ ...item,
+ outQty: Number.isFinite(numericValue) ? numericValue : 0
+ }
+ })
+ }
+
+ function handleClearBasket() {
+ basketRows.value = []
+ taskForm.value = createOutBoundTaskState()
+ }
+
+ async function handleGenerateTask() {
+ const message = getOutBoundValidationMessage({
+ siteNo: taskForm.value.siteNo,
+ items: basketRows.value
+ })
+ if (message) {
+ ElMessage.warning(message)
+ return
+ }
+
+ try {
+ generating.value = true
+ await ElMessageBox.confirm('纭畾鏍规嵁褰撳墠浠诲姟绡敓鎴愬嚭搴撲换鍔″悧锛�', '鐢熸垚纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ await fetchGenerateOutboundTask(
+ normalizeOutboundTaskPayload(
+ buildOutBoundGenerateTaskPayload({
+ siteNo: taskForm.value.siteNo,
+ memo: taskForm.value.memo,
+ items: normalizeOutBoundBasketRows(basketRows.value)
+ })
+ )
+ )
+ ElMessage.success('浠诲姟鐢熸垚鎴愬姛')
+ basketRows.value = []
+ taskForm.value = createOutBoundTaskState()
+ await refreshData()
+ } catch (error) {
+ if (error === 'cancel' || error?.message === 'cancel') {
+ return
+ }
+ ElMessage.error(error?.message || '鐢熸垚浠诲姟澶辫触')
+ } finally {
+ generating.value = false
+ }
+ }
+
+ const { columns, columnChecks } = useTableColumns(() =>
+ createOutBoundInventoryTableColumns({
+ handleViewDetail: openDetailDrawer,
+ handleAddToBasket
+ })
+ )
+
+ const basketColumns = computed(() =>
+ createOutBoundBasketTableColumns({
+ handleRemoveBasketRow,
+ handleOutQtyChange
+ })
+ )
+
+ const {
+ data,
+ loading,
+ pagination,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData,
+ getData
+ } = useTable({
+ core: {
+ apiFn: fetchOutboundInventoryPage,
+ apiParams: buildOutBoundPageQueryParams(searchForm.value),
+ columnsFactory: () => createOutBoundInventoryTableColumns({
+ handleViewDetail: openDetailDrawer,
+ handleAddToBasket
+ })
+ },
+ transform: {
+ dataTransformer: (records) => (Array.isArray(records) ? records.map((record) => normalizeOutBoundRow(record)) : [])
+ }
+ })
+
+ async function loadSiteOptions() {
+ const records = await guardRequestWithMessage(fetchOutboundSiteList(), [], {
+ timeoutMessage: '鍑哄簱绔欑偣鍔犺浇瓒呮椂锛屽凡鍋滄绛夊緟'
+ })
+ siteOptions.value = normalizeOutBoundSiteOptions(records)
+ }
+
+ function handleSearch(params) {
+ searchForm.value = {
+ ...searchForm.value,
+ ...params
+ }
+ replaceSearchParams(buildOutBoundPageQueryParams(searchForm.value))
+ getData()
+ }
+
+ function handleReset() {
+ searchForm.value = createOutBoundSearchState()
+ resetSearchParams()
+ }
+
+ onMounted(async () => {
+ await loadSiteOptions()
+ })
+</script>
diff --git a/rsf-design/src/views/work/out-bound/modules/out-bound-detail-drawer.vue b/rsf-design/src/views/work/out-bound/modules/out-bound-detail-drawer.vue
new file mode 100644
index 0000000..7f14ecf
--- /dev/null
+++ b/rsf-design/src/views/work/out-bound/modules/out-bound-detail-drawer.vue
@@ -0,0 +1,41 @@
+<template>
+ <ElDrawer :model-value="visible" title="搴撳瓨鏄庣粏璇︽儏" size="72%" @update:model-value="handleVisibleChange">
+ <div class="flex h-full flex-col gap-4">
+ <ElDescriptions :column="3" border>
+ <ElDescriptionsItem label="搴撲綅缂栫爜">{{ detail.locCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡缂栫爜">{{ detail.matnrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐗╂枡鍚嶇О">{{ detail.maktx || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵规">{{ detail.batch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗘壒娆�">{{ detail.splrBatch || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="杩借釜鐮�">{{ detail.trackCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍗曚綅">{{ detail.unit || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍙敤鏁伴噺">{{ detail.anfme ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鎵ц鏁伴噺">{{ detail.workQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鍑哄簱鏁伴噺">{{ detail.outQty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="瀹屾垚鏁伴噺">{{ detail.qty ?? '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鐘舵��">{{ detail.statusText || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="绔欑偣鍙�">{{ detail.siteNo || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+
+ <ElDescriptions :column="2" border>
+ <ElDescriptionsItem label="渚涘簲鍟嗙紪鐮�">{{ detail.splrCode || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="渚涘簲鍟嗗悕绉�">{{ detail.splrName || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="澶囨敞">{{ detail.memo || '--' }}</ElDescriptionsItem>
+ <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElDrawer>
+</template>
+
+<script setup>
+ defineProps({
+ visible: { type: Boolean, default: false },
+ detail: { type: Object, default: () => ({}) }
+ })
+
+ const emit = defineEmits(['update:visible'])
+
+ function handleVisibleChange(visible) {
+ emit('update:visible', visible)
+ }
+</script>
diff --git a/rsf-design/src/views/work/out-bound/outBoundPage.helpers.js b/rsf-design/src/views/work/out-bound/outBoundPage.helpers.js
new file mode 100644
index 0000000..0c91f8e
--- /dev/null
+++ b/rsf-design/src/views/work/out-bound/outBoundPage.helpers.js
@@ -0,0 +1,239 @@
+const OUT_BOUND_STATUS_META = {
+ 0: { label: '鍐荤粨', tagType: 'danger' },
+ 1: { label: '姝e父', tagType: 'success' }
+}
+
+function normalizeText(value) {
+ return String(value ?? '').trim()
+}
+
+function normalizeOptionalText(value) {
+ const text = normalizeText(value)
+ return text === '-' ? '' : text
+}
+
+function normalizeNumber(value) {
+ if (value === '' || value === null || value === undefined) {
+ return 0
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : 0
+}
+
+function getStatusMeta(status, statusText) {
+ const fallback = OUT_BOUND_STATUS_META[Number(status)] || {
+ label: statusText || '-',
+ tagType: 'info'
+ }
+ return {
+ label: statusText || fallback.label,
+ tagType: fallback.tagType
+ }
+}
+
+function toNumberOrOriginal(value) {
+ if (value === '' || value === null || value === undefined) {
+ return value
+ }
+ const numericValue = Number(value)
+ return Number.isFinite(numericValue) ? numericValue : value
+}
+
+function normalizeItemOutQty(row = {}) {
+ const outQty = row.outQty !== undefined && row.outQty !== null && row.outQty !== ''
+ ? normalizeNumber(row.outQty)
+ : normalizeNumber(row.anfme)
+ return outQty < 0 ? 0 : outQty
+}
+
+function compactDefinedFields(record = {}) {
+ return Object.fromEntries(
+ Object.entries(record).filter(([, value]) => value !== undefined && value !== null && value !== '')
+ )
+}
+
+export function createOutBoundSearchState() {
+ return {
+ condition: '',
+ locCode: '',
+ matnrCode: '',
+ maktx: '',
+ batch: '',
+ splrBatch: '',
+ trackCode: '',
+ status: '',
+ memo: ''
+ }
+}
+
+export function createOutBoundTaskState() {
+ return {
+ siteNo: '',
+ memo: ''
+ }
+}
+
+export function buildOutBoundSearchParams(params = {}) {
+ const result = {}
+ ;['condition', 'locCode', 'matnrCode', 'maktx', 'batch', 'splrBatch', 'trackCode', 'memo'].forEach((key) => {
+ const value = normalizeText(params[key])
+ if (value) {
+ result[key] = value
+ }
+ })
+
+ if (params.status !== '' && params.status !== undefined && params.status !== null) {
+ result.status = Number(params.status)
+ }
+
+ return result
+}
+
+export function buildOutBoundPageQueryParams(params = {}) {
+ return {
+ current: params.current || 1,
+ pageSize: params.pageSize || params.size || 20,
+ ...buildOutBoundSearchParams(params)
+ }
+}
+
+export function normalizeOutBoundRow(record = {}) {
+ const statusMeta = getStatusMeta(record.status, record['status$'])
+ return {
+ ...record,
+ locCode: normalizeText(record.locCode) || '-',
+ matnrCode: normalizeText(record.matnrCode) || '-',
+ maktx: normalizeText(record.maktx) || '-',
+ trackCode: normalizeText(record.trackCode) || '-',
+ batch: normalizeText(record.batch) || '-',
+ splrBatch: normalizeText(record.splrBatch) || '-',
+ unit: normalizeText(record.unit) || '-',
+ anfme: normalizeNumber(record.anfme),
+ workQty: normalizeNumber(record.workQty),
+ qty: normalizeNumber(record.qty),
+ outQty: normalizeNumber(record.outQty),
+ statusText: statusMeta.label,
+ statusTagType: statusMeta.tagType,
+ createTimeText: normalizeText(record['createTime$'] || record.createTime) || '-',
+ updateTimeText: normalizeText(record['updateTime$'] || record.updateTime) || '-',
+ splrCode: normalizeText(record.splrCode) || '-',
+ splrName: normalizeText(record.splrName) || '-',
+ siteNo: normalizeText(record.siteNo) || '-',
+ memo: normalizeText(record.memo) || '-'
+ }
+}
+
+export function normalizeOutBoundBasketRows(rows = []) {
+ if (!Array.isArray(rows)) {
+ return []
+ }
+
+ const seen = new Set()
+ return rows
+ .map((row) => ({
+ ...normalizeOutBoundRow(row),
+ outQty: normalizeItemOutQty(row)
+ }))
+ .filter((row) => {
+ const key = row.id ?? `${row.locId || ''}-${row.matnrCode || ''}-${row.batch || ''}`
+ if (seen.has(key)) {
+ return false
+ }
+ seen.add(key)
+ return true
+ })
+}
+
+export function mergeOutBoundBasketRows(currentRows = [], incomingRows = []) {
+ const basket = new Map()
+
+ normalizeOutBoundBasketRows(currentRows).forEach((row) => {
+ const key = row.id ?? `${row.locId || ''}-${row.matnrCode || ''}-${row.batch || ''}`
+ basket.set(key, row)
+ })
+
+ normalizeOutBoundBasketRows(incomingRows).forEach((row) => {
+ const key = row.id ?? `${row.locId || ''}-${row.matnrCode || ''}-${row.batch || ''}`
+ if (!basket.has(key)) {
+ basket.set(key, row)
+ return
+ }
+ basket.set(key, {
+ ...basket.get(key),
+ ...row,
+ outQty: basket.get(key).outQty > 0 ? basket.get(key).outQty : row.outQty
+ })
+ })
+
+ return Array.from(basket.values())
+}
+
+export function buildOutBoundGenerateTaskPayload({ siteNo, memo = '', items = [] } = {}) {
+ return {
+ siteNo: normalizeOptionalText(siteNo),
+ memo: normalizeText(memo),
+ items: Array.isArray(items)
+ ? items.map((row) =>
+ compactDefinedFields({
+ ...row,
+ id: toNumberOrOriginal(row.id),
+ locId: toNumberOrOriginal(row.locId),
+ orderId: toNumberOrOriginal(row.orderId),
+ orderItemId: toNumberOrOriginal(row.orderItemId),
+ wkType: toNumberOrOriginal(row.wkType),
+ matnrId: toNumberOrOriginal(row.matnrId),
+ channel: toNumberOrOriginal(row.channel),
+ status: toNumberOrOriginal(row.status),
+ outQty: normalizeItemOutQty(row),
+ anfme: normalizeNumber(row.anfme),
+ workQty: normalizeNumber(row.workQty),
+ qty: normalizeNumber(row.qty),
+ siteNo: normalizeOptionalText(row.siteNo),
+ memo: normalizeOptionalText(row.memo)
+ })
+ )
+ : []
+ }
+}
+
+export function normalizeOutBoundSiteOptions(records = []) {
+ if (!Array.isArray(records)) {
+ return []
+ }
+
+ const seen = new Set()
+ return records
+ .map((record) => normalizeText(record.site))
+ .filter((site) => {
+ if (!site || seen.has(site)) {
+ return false
+ }
+ seen.add(site)
+ return true
+ })
+ .map((site) => ({
+ label: site,
+ value: site
+ }))
+}
+
+export function getOutBoundValidationMessage({ siteNo, items = [] } = {}) {
+ if (!normalizeText(siteNo)) {
+ return '璇烽�夋嫨鍑哄簱绔欑偣'
+ }
+ if (!Array.isArray(items) || items.length === 0) {
+ return '璇峰厛閫夋嫨搴撳瓨鏄庣粏'
+ }
+
+ const invalidRow = items.find((row) => normalizeItemOutQty(row) <= 0)
+ if (invalidRow) {
+ return `搴撳瓨鏄庣粏 ${invalidRow.locCode || invalidRow.matnrCode || invalidRow.id || ''} 鐨勫嚭搴撴暟閲忓繀椤诲ぇ浜� 0`
+ }
+
+ const overflowRow = items.find((row) => normalizeItemOutQty(row) + normalizeNumber(row.workQty) > normalizeNumber(row.anfme))
+ if (overflowRow) {
+ return `搴撳瓨鏄庣粏 ${overflowRow.locCode || overflowRow.matnrCode || overflowRow.id || ''} 鐨勫嚭搴撴暟閲忎笉鑳借秴杩囧彲鐢ㄦ暟閲廯
+ }
+
+ return ''
+}
diff --git a/rsf-design/src/views/work/out-bound/outBoundTable.columns.js b/rsf-design/src/views/work/out-bound/outBoundTable.columns.js
new file mode 100644
index 0000000..171d9d6
--- /dev/null
+++ b/rsf-design/src/views/work/out-bound/outBoundTable.columns.js
@@ -0,0 +1,203 @@
+import { h } from 'vue'
+import { ElInputNumber, ElTag } from 'element-plus'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+
+export function createOutBoundInventoryTableColumns({ handleViewDetail, handleAddToBasket } = {}) {
+ return [
+ { type: 'selection', width: 48, align: 'center' },
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locCode || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrBatch || '--'
+ },
+ {
+ prop: 'trackCode',
+ label: '杩借釜鐮�',
+ minWidth: 160,
+ showOverflowTooltip: true,
+ formatter: (row) => row.trackCode || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鍙敤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'qty',
+ label: '瀹屾垚鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'statusText',
+ label: '鐘舵��',
+ width: 100,
+ formatter: (row) =>
+ h(
+ ElTag,
+ { type: row.statusTagType || 'info', effect: 'light' },
+ () => row.statusText || '--'
+ )
+ },
+ {
+ prop: 'updateTimeText',
+ label: '鏇存柊鏃堕棿',
+ minWidth: 170,
+ showOverflowTooltip: true,
+ formatter: (row) => row.updateTimeText || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 170,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonMore, {
+ list: [
+ { key: 'view', label: '鏌ョ湅璇︽儏', icon: 'ri:eye-line' },
+ { key: 'add', label: '鍔犲叆浠诲姟', icon: 'ri:add-line' }
+ ],
+ onClick: (item) => {
+ if (item.key === 'view') handleViewDetail?.(row)
+ if (item.key === 'add') handleAddToBasket?.([row])
+ }
+ })
+ }
+ ]
+}
+
+export function createOutBoundBasketTableColumns({ handleRemoveBasketRow, handleOutQtyChange } = {}) {
+ return [
+ { type: 'globalIndex', label: '搴忓彿', width: 72, align: 'center' },
+ {
+ prop: 'locCode',
+ label: '搴撲綅缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.locCode || '--'
+ },
+ {
+ prop: 'matnrCode',
+ label: '鐗╂枡缂栫爜',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.matnrCode || '--'
+ },
+ {
+ prop: 'maktx',
+ label: '鐗╂枡鍚嶇О',
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatter: (row) => row.maktx || '--'
+ },
+ {
+ prop: 'batch',
+ label: '鎵规',
+ minWidth: 130,
+ showOverflowTooltip: true,
+ formatter: (row) => row.batch || '--'
+ },
+ {
+ prop: 'splrBatch',
+ label: '渚涘簲鍟嗘壒娆�',
+ minWidth: 150,
+ showOverflowTooltip: true,
+ formatter: (row) => row.splrBatch || '--'
+ },
+ {
+ prop: 'anfme',
+ label: '鍙敤鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'workQty',
+ label: '鎵ц鏁伴噺',
+ width: 110,
+ align: 'right'
+ },
+ {
+ prop: 'outQty',
+ label: '鍑哄簱鏁伴噺',
+ minWidth: 140,
+ align: 'right',
+ formatter: (row) =>
+ h(ElInputNumber, {
+ modelValue: Number(row.outQty ?? row.anfme ?? 0),
+ min: 0,
+ precision: 3,
+ controlsPosition: 'right',
+ style: 'width: 100%',
+ 'onUpdate:modelValue': (value) => handleOutQtyChange?.(row, value)
+ })
+ },
+ {
+ prop: 'unit',
+ label: '鍗曚綅',
+ width: 90
+ },
+ {
+ prop: 'siteNo',
+ label: '绔欑偣鍙�',
+ minWidth: 140,
+ showOverflowTooltip: true,
+ formatter: (row) => row.siteNo || '--'
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 90,
+ fixed: 'right',
+ formatter: (row) =>
+ h(ArtButtonTable, {
+ icon: 'ri:delete-bin-5-line',
+ title: '绉婚櫎',
+ onClick: () => handleRemoveBasketRow?.(row)
+ })
+ }
+ ]
+}
diff --git a/rsf-design/tests/auth-contract.test.mjs b/rsf-design/tests/auth-contract.test.mjs
deleted file mode 100644
index 6dd0d4e..0000000
--- a/rsf-design/tests/auth-contract.test.mjs
+++ /dev/null
@@ -1,123 +0,0 @@
-import assert from 'node:assert/strict'
-import { register } from 'node:module'
-import test from 'node:test'
-
-register(
- 'data:text/javascript,export function resolve(specifier, context, nextResolve){ if(specifier===\'@/utils/http\'){ return { shortCircuit:true, url:\'data:text/javascript,export default { post(options){ globalThis.__authHttpCalls.push({ method:"post", options }); return Promise.resolve({ accessToken:"abc", refreshToken:"ref", user:{ id:1 } }) }, get(options){ globalThis.__authHttpCalls.push({ method:"get", options }); return Promise.resolve({ userId:1, roles:[{ code:"R_ADMIN", name:"绠$悊鍛�" }], authorities:[{ authority:"system:menu:save" },{ authority:"system:menu:update" }] }) } }\' } } return nextResolve(specifier, context) }',
- import.meta.url
-)
-
-globalThis.__authHttpCalls = []
-
-const {
- buildLoginPayload,
- fetchGetUserInfo,
- fetchLogin,
- normalizeLoginResponse,
- normalizeUserInfo
-} = await import('../src/api/auth.js')
-
-test('buildLoginPayload matches the rsf-server login contract', () => {
- assert.deepEqual(buildLoginPayload({ username: 'demo', password: '123456' }), {
- username: 'demo',
- password: '123456'
- })
-})
-
-test('normalizeLoginResponse extracts the real token fields', () => {
- assert.deepEqual(
- normalizeLoginResponse({
- code: 200,
- data: { accessToken: 'abc', user: { id: 1 } }
- }),
- { accessToken: 'abc', refreshToken: '', user: { id: 1 } }
- )
-})
-
-test('normalizeLoginResponse also handles an inner data object', () => {
- assert.deepEqual(
- normalizeLoginResponse({
- accessToken: 'abc',
- refreshToken: 'ref',
- user: { id: 1 }
- }),
- { accessToken: 'abc', refreshToken: 'ref', user: { id: 1 } }
- )
-})
-
-test('normalizeLoginResponse accepts the old backend accessToken shape', () => {
- const result = normalizeLoginResponse({
- code: 200,
- data: { accessToken: 'token-1', refreshToken: '', user: { username: 'admin' } }
- })
-
- assert.equal(result.accessToken, 'token-1')
-})
-
-test('normalizeLoginResponse keeps missing accessToken empty', () => {
- const result = normalizeLoginResponse({
- code: 200,
- data: { refreshToken: 'ref', user: { username: 'admin' } }
- })
-
- assert.equal(result.accessToken, '')
-})
-
-test('normalizeUserInfo returns roles and buttons arrays safely', () => {
- assert.deepEqual(normalizeUserInfo({ userId: 1, roles: ['R_ADMIN'], buttons: ['add'] }), {
- userId: 1,
- roles: ['R_ADMIN'],
- buttons: ['add']
- })
-})
-
-test('normalizeUserInfo derives role codes and button aliases from rsf-server auth payloads', () => {
- assert.deepEqual(
- normalizeUserInfo({
- userId: 1,
- roles: [{ code: 'R_SUPER', name: '瓒呯骇绠$悊鍛�' }],
- authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }]
- }),
- {
- userId: 1,
- roles: ['R_SUPER'],
- authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }],
- buttons: ['system:menu:save', 'system:menu:update']
- }
- )
-})
-
-test('fetchLogin keeps supporting legacy userName input at the boundary', async () => {
- const result = await fetchLogin({ userName: 'demo', password: '123456' })
-
- assert.deepEqual(globalThis.__authHttpCalls.at(-1), {
- method: 'post',
- options: {
- url: '/login',
- params: { username: 'demo', password: '123456' }
- }
- })
- assert.deepEqual(result, {
- token: 'abc',
- accessToken: 'abc',
- refreshToken: 'ref',
- user: { id: 1 }
- })
-})
-
-test('fetchGetUserInfo normalizes the returned user payload', async () => {
- const result = await fetchGetUserInfo()
-
- assert.deepEqual(globalThis.__authHttpCalls.at(-1), {
- method: 'get',
- options: {
- url: '/auth/user'
- }
- })
- assert.deepEqual(result, {
- userId: 1,
- roles: ['R_ADMIN'],
- authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }],
- buttons: ['system:menu:save', 'system:menu:update']
- })
-})
diff --git a/rsf-design/tests/backend-api-base.test.mjs b/rsf-design/tests/backend-api-base.test.mjs
deleted file mode 100644
index ded0f78..0000000
--- a/rsf-design/tests/backend-api-base.test.mjs
+++ /dev/null
@@ -1,30 +0,0 @@
-import assert from 'node:assert/strict'
-import { readFile } from 'node:fs/promises'
-import path from 'node:path'
-import test from 'node:test'
-
-const projectRoot = path.resolve(import.meta.dirname, '..')
-
-async function readProjectFile(relativePath) {
- return readFile(path.join(projectRoot, relativePath), 'utf8')
-}
-
-test('development env routes API requests through the rsf-server context path', async () => {
- const envContent = await readProjectFile('.env.development')
-
- assert.match(envContent, /VITE_API_URL\s*=\s*\/rsf-server\b/)
- assert.match(envContent, /VITE_API_PROXY_URL\s*=\s*http:\/\/127\.0\.0\.1:8085\b/)
-})
-
-test('production env keeps the rsf-server context path as the API base', async () => {
- const envContent = await readProjectFile('.env.production')
-
- assert.match(envContent, /VITE_API_URL\s*=\s*\/rsf-server\b/)
-})
-
-test('vite dev server proxies the rsf-server context path to the backend target', async () => {
- const viteConfig = await readProjectFile('vite.config.js')
-
- assert.match(viteConfig, /'\/rsf-server'\s*:\s*\{/)
- assert.match(viteConfig, /target:\s*VITE_API_PROXY_URL/)
-})
diff --git a/rsf-design/tests/backend-menu-adapter.test.mjs b/rsf-design/tests/backend-menu-adapter.test.mjs
deleted file mode 100644
index 76d6c49..0000000
--- a/rsf-design/tests/backend-menu-adapter.test.mjs
+++ /dev/null
@@ -1,208 +0,0 @@
-import assert from 'node:assert/strict'
-import fs from 'node:fs'
-import test from 'node:test'
-import { RoutesAlias } from '../src/router/routesAlias.js'
-
-test('adapts a backend menu tree using route for path and name for title', async () => {
- const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js')
- const result = adaptBackendMenuTree([
- {
- id: 10,
- name: 'menu.system',
- parentId: 0,
- path: '1',
- route: '/system',
- type: 0,
- children: [
- {
- id: 11,
- name: 'menu.userLogin',
- parentId: 10,
- path: '1,10',
- route: 'user-login',
- component: 'userLogin',
- type: 0
- }
- ]
- }
- ])
-
- assert.equal(result[0].path, 'system')
- assert.equal(result[0].meta.title, '绯荤粺璁剧疆')
- assert.equal(result[0].children[0].path, 'user-login')
- assert.equal(result[0].children[0].meta.title, '鐧诲綍鏃ュ織')
- assert.equal(result[0].children[0].component, '/system/user-login')
-})
-
-test('keeps backend leaves by deriving component paths from the route hierarchy when no alias exists', async () => {
- const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js')
- const result = adaptBackendMenuTree([
- {
- id: 1,
- name: 'menu.system',
- parentId: 0,
- path: '1',
- route: '/system',
- type: 0,
- children: [
- {
- id: 2,
- name: 'menu.userLogin',
- parentId: 1,
- path: '1,2',
- route: 'user-login',
- component: 'userLogin',
- type: 0
- },
- {
- id: 3,
- name: 'menu.host',
- parentId: 1,
- path: '1,3',
- route: 'host',
- component: 'host',
- type: 0
- }
- ]
- }
- ])
-
- assert.equal(result[0].children.length, 2)
- assert.equal(result[0].children[0].path, 'user-login')
- assert.equal(result[0].children[0].meta.title, '鐧诲綍鏃ュ織')
- assert.equal(result[0].children[0].component, '/system/user-login')
- assert.equal(result[0].children[1].path, 'host')
- assert.equal(result[0].children[1].meta.title, '鏈烘瀯绠$悊')
- assert.equal(result[0].children[1].component, '/system/host')
-})
-
-test('filters non-menu nodes while keeping plain string titles unchanged', async () => {
- const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js')
- const result = adaptBackendMenuTree([
- {
- id: 20,
- name: '绯荤粺璁剧疆',
- parentId: 0,
- path: '20',
- route: '/system',
- type: 0,
- children: [
- {
- id: 21,
- name: 'menu.user',
- parentId: 20,
- path: '20,21',
- route: 'user',
- component: 'user',
- type: 0
- },
- {
- id: 22,
- name: 'system:user:add',
- parentId: 20,
- path: '20,22',
- route: 'user/add',
- component: 'user',
- type: 1
- }
- ]
- }
- ])
-
- assert.equal(result[0].meta.title, '绯荤粺璁剧疆')
- assert.equal(result[0].children.length, 1)
- assert.equal(result[0].children[0].id, '21')
- assert.equal(result[0].children[0].component, '/system/user')
-})
-
-test('preserves absolute backend child routes instead of nesting them under the parent path', async () => {
- const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js')
- const result = adaptBackendMenuTree([
- {
- id: 5318,
- name: 'AI绠$悊涓績',
- parentId: 0,
- route: '/AI',
- type: 0,
- children: [
- {
- id: 422,
- name: 'menu.aiParam',
- parentId: 5318,
- route: '/system/aiParam',
- component: 'aiParam',
- type: 0
- }
- ]
- }
- ])
-
- assert.equal(result[0].path, 'AI')
- assert.equal(result[0].children[0].path, '/system/aiParam')
- assert.equal(result[0].children[0].meta.title, 'AI 鍙傛暟')
- assert.equal(result[0].children[0].component, '/system/aiParam')
-})
-
-test('keeps directory menus as layout nodes when they only group child routes', async () => {
- const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js')
- const result = adaptBackendMenuTree([
- {
- id: 5318,
- name: 'AI绠$悊涓績',
- parentId: 0,
- route: '/AI',
- type: 0,
- children: [
- {
- id: 422,
- name: 'menu.aiParam',
- parentId: 5318,
- route: '/system/aiParam',
- component: 'aiParam',
- type: 0
- }
- ]
- }
- ])
-
- assert.equal(result[0].component, RoutesAlias.Layout)
-})
-
-test('maps legacy backend icon names into renderable Iconify icons', async () => {
- const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js')
- const result = adaptBackendMenuTree([
- {
- id: 1,
- name: 'AI绠$悊涓績',
- route: '/ai',
- icon: 'SmartToy',
- type: 0,
- children: [
- {
- id: 2,
- name: 'menu.userLogin',
- route: 'user-login',
- component: 'userLogin',
- icon: 'Token',
- type: 0
- }
- ]
- }
- ])
-
- assert.equal(result[0].meta.icon, 'ri:robot-2-line')
- assert.equal(result[0].children[0].meta.icon, 'ri:key-2-line')
-})
-
-test('phase 1 resource mappings resolve to existing views and translatable menu labels', async () => {
- const { PHASE_1_COMPONENTS } = await import('../src/router/adapters/backendMenuAdapter.js')
-
- Object.entries(PHASE_1_COMPONENTS).forEach(([resourceKey, viewPath]) => {
- const normalizedPath = viewPath.replace(/^\//, '')
- const componentWithIndex = new URL(`../src/views/${normalizedPath}/index.vue`, import.meta.url)
- const singleFileComponent = new URL(`../src/views/${normalizedPath}.vue`, import.meta.url)
- const componentExists = fs.existsSync(componentWithIndex) || fs.existsSync(singleFileComponent)
-
- assert.equal(componentExists, true, `${resourceKey} should resolve to an existing view`)
- })
-})
diff --git a/rsf-design/tests/basic-info-loc-page-contract.test.mjs b/rsf-design/tests/basic-info-loc-page-contract.test.mjs
deleted file mode 100644
index a7f5dee..0000000
--- a/rsf-design/tests/basic-info-loc-page-contract.test.mjs
+++ /dev/null
@@ -1,266 +0,0 @@
-import assert from 'node:assert/strict'
-import { readFileSync } from 'node:fs'
-import test from 'node:test'
-
-const pageModuleUrl = new URL('../src/views/basic-info/loc/index.vue', import.meta.url)
-const helpersModuleUrl = new URL('../src/views/basic-info/loc/locPage.helpers.js', import.meta.url)
-const columnsModuleUrl = new URL('../src/views/basic-info/loc/locTable.columns.js', import.meta.url)
-const apiModuleUrl = new URL('../src/api/loc.js', import.meta.url)
-const backendMenuAdapterUrl = new URL('../src/router/adapters/backendMenuAdapter.js', import.meta.url)
-const staticRoutesUrl = new URL('../src/router/routes/staticRoutes.js', import.meta.url)
-
-test('loc api exposes the dedicated basic-info backend contract', async () => {
- const apiSource = readFileSync(apiModuleUrl, 'utf8')
-
- assert.match(apiSource, /fetchLocPage/)
- assert.match(apiSource, /fetchGetLocDetail/)
- assert.match(apiSource, /fetchGetLocMany/)
- assert.match(apiSource, /fetchSaveLoc/)
- assert.match(apiSource, /fetchUpdateLoc/)
- assert.match(apiSource, /fetchDeleteLoc/)
- assert.match(apiSource, /fetchLocQuery/)
- assert.match(apiSource, /fetchLocTypeList/)
- assert.match(apiSource, /fetchWarehouseList/)
- assert.match(apiSource, /fetchWarehouseAreasList/)
- assert.match(apiSource, /fetchExportLocReport/)
- assert.match(apiSource, /url:\s*'\/loc\/page'/)
- assert.match(apiSource, /url:\s*'\/loc\/save'/)
- assert.match(apiSource, /url:\s*'\/loc\/update'/)
- assert.match(apiSource, /url:\s*`\/loc\/remove\/\$\{normalizeIds\(ids\)\}`/)
- assert.match(apiSource, /url:\s*'\/locType\/list'/)
-})
-
-test('loc helpers keep page, save and detail contracts stable', async () => {
- const helpers = await import(helpersModuleUrl)
-
- assert.deepEqual(helpers.createLocSearchState(), {
- condition: '',
- warehouseId: '',
- areaId: '',
- code: '',
- useStatus: '',
- row: '',
- col: '',
- lev: '',
- channel: '',
- status: '',
- barcode: '',
- memo: ''
- })
-
- assert.deepEqual(helpers.getLocPaginationKey(), {
- current: 'current',
- size: 'pageSize'
- })
-
- assert.deepEqual(
- helpers.buildLocPageQueryParams({
- current: 2,
- pageSize: 30,
- condition: ' A鍖� ',
- warehouseId: '8',
- areaId: '9',
- code: ' LOC-01 ',
- useStatus: ' O ',
- row: '1',
- col: '2',
- lev: '3',
- channel: '4',
- status: '1',
- barcode: ' BOX-01 ',
- memo: ' memo '
- }),
- {
- current: 2,
- pageSize: 30,
- condition: 'A鍖�',
- warehouseId: 8,
- areaId: 9,
- code: 'LOC-01',
- useStatus: 'O',
- row: 1,
- col: 2,
- lev: 3,
- channel: 4,
- status: 1,
- barcode: 'BOX-01',
- memo: 'memo'
- }
- )
-
- assert.deepEqual(
- helpers.buildLocSavePayload({
- id: '8',
- version: '3',
- warehouseId: '5',
- areaId: '6',
- code: ' LOC-01 ',
- typeIds: ['1', '2'],
- flagLogic: 1,
- fucAtrrs: ' 鍔熻兘 ',
- barcode: ' BOX-01 ',
- unit: ' 绠� ',
- length: '1.2',
- height: '2.3',
- width: '3.4',
- row: '1',
- col: '2',
- lev: '3',
- channel: '4',
- maxParts: '10',
- maxPack: '20',
- useStatus: 'F',
- flagLabelMange: 0,
- locAttrs: ' 灞炴�� ',
- status: '',
- memo: ' 澶囨敞 '
- }),
- {
- id: 8,
- version: 3,
- warehouseId: 5,
- areaId: 6,
- code: 'LOC-01',
- typeIds: [1, 2],
- flagLogic: 1,
- fucAtrrs: '鍔熻兘',
- barcode: 'BOX-01',
- unit: '绠�',
- length: 1.2,
- height: 2.3,
- width: 3.4,
- row: 1,
- col: 2,
- lev: 3,
- channel: 4,
- maxParts: 10,
- maxPack: 20,
- useStatus: 'F',
- flagLabelMange: 0,
- locAttrs: '灞炴��',
- status: 1,
- memo: '澶囨敞'
- }
- )
-
- const detail = helpers.normalizeLocDetailRecord({
- id: 1,
- warehouseId: 4,
- warehouseId$: '涓讳粨',
- areaId: 8,
- areaId$: 'A鍖�',
- type: '1,2',
- typeIds$: '楂樺簱浣�,涓簱浣�',
- useStatus: 'O',
- status: 1,
- flagLogic: 1,
- flagLabelMange: 0,
- code: ' LOC-01 ',
- barcode: ' BOX-01 ',
- unit: ' 绠� ',
- row: 1,
- col: 2,
- lev: 3,
- channel: 4,
- length: 1.2,
- height: 2.3,
- width: 3.4,
- maxParts: 10,
- maxPack: 20,
- memo: ' memo ',
- createBy$: 'ROOT',
- updateBy$: 'ROOT',
- createTime$: '2026-03-30 10:00:00',
- updateTime$: '2026-03-30 10:10:00'
- })
-
- assert.equal(detail.warehouseName, '涓讳粨')
- assert.equal(detail.areaName, 'A鍖�')
- assert.equal(detail.typeIdsText, '楂樺簱浣�,涓簱浣�')
- assert.equal(detail.useStatusText, '绌哄簱')
- assert.equal(detail.statusText, '姝e父')
- assert.equal(detail.flagLogicText, '鏄�')
- assert.equal(detail.flagLabelMangeText, '鍚�')
- assert.equal(detail.memo, 'memo')
-
- assert.deepEqual(helpers.buildLocPrintRows([{ id: 2, code: 'LOC-02', useStatus: 'F', status: 0 }]), [
- {
- id: 2,
- code: 'LOC-02',
- useStatus: 'F',
- status: 0,
- warehouseName: '',
- areaName: '',
- typeIds: [],
- typeIdsText: '',
- barcode: '',
- unit: '',
- fucAtrrs: '',
- locAttrs: '',
- memo: '',
- flagLogicText: '--',
- flagLabelMangeText: '--',
- useStatusText: '鍦ㄥ簱',
- useStatusType: 'primary',
- statusText: '鍐荤粨',
- statusType: 'danger',
- statusBool: false,
- row: void 0,
- col: void 0,
- lev: void 0,
- channel: void 0,
- length: void 0,
- height: void 0,
- width: void 0,
- maxParts: void 0,
- maxPack: void 0,
- createByText: '',
- createTimeText: '',
- updateByText: '',
- updateTimeText: ''
- }
- ])
-
- assert.deepEqual(helpers.buildLocReportMeta({ count: 12 }), {
- reportTitle: '搴撲綅鎶ヨ〃',
- reportDate: undefined,
- printedAt: undefined,
- operator: undefined,
- count: 12,
- reportStyle: {
- titleAlign: 'center',
- titleLevel: 'strong',
- orientation: 'portrait',
- density: 'compact',
- showSequence: true
- }
- })
-})
-
-test('loc page skeleton uses ArtDesignPro list page structure', async () => {
- const pageSource = readFileSync(pageModuleUrl, 'utf8')
- const columnsSource = readFileSync(columnsModuleUrl, 'utf8')
-
- assert.match(pageSource, /ArtSearchBar/)
- assert.match(pageSource, /ArtTableHeader/)
- assert.match(pageSource, /ListExportPrint/)
- assert.match(pageSource, /LocDialog/)
- assert.match(pageSource, /LocDetailDrawer/)
- assert.match(pageSource, /useCrudPage/)
- assert.match(pageSource, /usePrintExportPage/)
- assert.match(columnsSource, /label:\s*'搴撲綅鍙�'/)
- assert.match(columnsSource, /label:\s*'浠撳簱'/)
- assert.match(columnsSource, /label:\s*'搴撳尯'/)
- assert.match(columnsSource, /label:\s*'搴撲綅绫诲瀷'/)
- assert.match(columnsSource, /label:\s*'浣跨敤鐘舵��'/)
-})
-
-test('backend menu adapter and static routes expose the loc page', async () => {
- const backendMenuAdapterSource = readFileSync(backendMenuAdapterUrl, 'utf8')
- const staticRoutesSource = readFileSync(staticRoutesUrl, 'utf8')
-
- assert.match(backendMenuAdapterSource, /loc:\s*'\/basic-info\/loc'/)
- assert.match(staticRoutesSource, /path:\s*'loc'/)
- assert.match(staticRoutesSource, /title:\s*'menu\.loc'/)
- assert.match(staticRoutesSource, /basic-info\/loc\/index\.vue/)
-})
diff --git a/rsf-design/tests/basic-info-warehouse-areas-page-contract.test.mjs b/rsf-design/tests/basic-info-warehouse-areas-page-contract.test.mjs
deleted file mode 100644
index 280c44e..0000000
--- a/rsf-design/tests/basic-info-warehouse-areas-page-contract.test.mjs
+++ /dev/null
@@ -1,177 +0,0 @@
-import assert from 'node:assert/strict'
-import { readFile } from 'node:fs/promises'
-import { fileURLToPath } from 'node:url'
-import test from 'node:test'
-
-const apiPath = new URL('../src/api/warehouse-areas.js', import.meta.url)
-const helpersPath = new URL('../src/views/basic-info/warehouse-areas/warehouseAreasPage.helpers.js', import.meta.url)
-const columnsPath = new URL('../src/views/basic-info/warehouse-areas/warehouseAreasTable.columns.js', import.meta.url)
-const pagePath = new URL('../src/views/basic-info/warehouse-areas/index.vue', import.meta.url)
-const backendMenuAdapterPath = new URL('../src/router/adapters/backendMenuAdapter.js', import.meta.url)
-const staticRoutesPath = new URL('../src/router/routes/staticRoutes.js', import.meta.url)
-
-test('warehouse areas api uses real backend endpoints', async () => {
- const apiSource = await readFile(fileURLToPath(apiPath), 'utf8')
-
- assert.match(apiSource, /fetchWarehouseAreasPage/)
- assert.match(apiSource, /fetchWarehouseAreasDetail/)
- assert.match(apiSource, /fetchWarehouseAreasMany/)
- assert.match(apiSource, /fetchSaveWarehouseAreas/)
- assert.match(apiSource, /fetchUpdateWarehouseAreas/)
- assert.match(apiSource, /fetchDeleteWarehouseAreas/)
- assert.match(apiSource, /fetchWarehouseAreasQuery/)
- assert.match(apiSource, /fetchExportWarehouseAreasReport/)
- assert.match(apiSource, /url: '\/warehouseAreas\/page'/)
- assert.match(apiSource, /url: '\/warehouseAreas\/list'/)
- assert.match(apiSource, /url: `\/warehouseAreas\/many\/\$\{normalizeIds\(ids\)\}`/)
- assert.match(apiSource, /url: '\/warehouseAreas\/save'/)
- assert.match(apiSource, /url: '\/warehouseAreas\/update'/)
- assert.match(apiSource, /url: `\/warehouseAreas\/remove\/\$\{normalizeIds\(ids\)\}`/)
- assert.match(apiSource, /url: '\/warehouseAreas\/query'/)
- assert.match(apiSource, /warehouseAreas\/export/)
-})
-
-test('warehouse areas helpers keep page, save and detail contracts stable', async () => {
- const helpers = await import('../src/views/basic-info/warehouse-areas/warehouseAreasPage.helpers.js')
-
- assert.deepEqual(helpers.createWarehouseAreasSearchState(), {
- condition: '',
- warehouseId: '',
- code: '',
- name: '',
- type: '',
- shipperId: '',
- supplierId: '',
- status: '',
- memo: ''
- })
-
- assert.deepEqual(helpers.getWarehouseAreasPaginationKey(), {
- current: 'current',
- size: 'pageSize'
- })
-
- assert.deepEqual(
- helpers.buildWarehouseAreasPageQueryParams({
- current: 2,
- pageSize: 30,
- condition: ' 搴撳尯A ',
- warehouseId: 8,
- code: ' A01 ',
- name: ' 涓�妤煎簱鍖� ',
- type: ' normal ',
- shipperId: 5,
- supplierId: 7,
- status: 1,
- memo: ' memo '
- }),
- {
- current: 2,
- pageSize: 30,
- condition: '搴撳尯A',
- warehouseId: 8,
- code: 'A01',
- name: '涓�妤煎簱鍖�',
- type: 'normal',
- shipperId: 5,
- supplierId: 7,
- status: 1,
- memo: 'memo'
- }
- )
-
- assert.deepEqual(
- helpers.buildWarehouseAreasSavePayload({
- id: '9',
- warehouseId: '3',
- code: ' A01 ',
- name: ' 涓�妤煎簱鍖� ',
- type: ' normal ',
- shipperId: '11',
- supplierId: '12',
- flagMinus: 1,
- flagLabelMange: 0,
- flagMix: 1,
- status: '',
- sort: '2',
- memo: ' memo '
- }),
- {
- id: 9,
- warehouseId: 3,
- code: 'A01',
- name: '涓�妤煎簱鍖�',
- type: 'normal',
- shipperId: 11,
- supplierId: 12,
- flagMinus: 1,
- flagLabelMange: 0,
- flagMix: 1,
- status: 1,
- sort: 2,
- memo: 'memo'
- }
- )
-
- const detail = helpers.normalizeWarehouseAreasDetailRecord({
- id: 1,
- warehouseId: 4,
- warehouseId$: '涓讳粨',
- type: 'A',
- type$: '甯告俯',
- name: ' 涓�妤煎簱鍖� ',
- code: ' A01 ',
- shipperId$: '璐т富A',
- supplierId$: '渚涘簲鍟咮',
- flagMinus: 1,
- flagLabelMange: 0,
- flagMix: 1,
- status: 1,
- sort: 3,
- memo: ' memo ',
- createBy$: 'root',
- createTime$: '2026-03-30 10:00:00',
- updateBy$: 'root',
- updateTime$: '2026-03-30 10:10:00'
- })
-
- assert.equal(detail.statusText, '姝e父')
- assert.equal(detail.flagMixText, '鏄�')
- assert.equal(detail.warehouseName, '涓讳粨')
- assert.equal(detail.typeText, '甯告俯')
- assert.equal(detail.shipperName, '璐т富A')
- assert.equal(detail.supplierName, '渚涘簲鍟咮')
- assert.equal(detail.memo, 'memo')
-})
-
-test('warehouse areas columns expose detail action slot and status tag', async () => {
- const columnsSource = await readFile(fileURLToPath(columnsPath), 'utf8')
-
- assert.match(columnsSource, /createWarehouseAreasTableColumns/)
- assert.match(columnsSource, /ArtButtonMore|ArtButtonTable/)
- assert.match(columnsSource, /label: '鎿嶄綔'/)
- assert.match(columnsSource, /useSlot: true|formatter:/)
- assert.match(columnsSource, /label: '鐘舵��'/)
-})
-
-test('warehouse areas page uses real query, tree-like references and detail drawer structure', async () => {
- const pageSource = await readFile(fileURLToPath(pagePath), 'utf8')
-
- assert.match(pageSource, /fetchWarehouseAreasPage/)
- assert.match(pageSource, /fetchWarehouseAreasDetail/)
- assert.match(pageSource, /fetchSaveWarehouseAreas/)
- assert.match(pageSource, /fetchUpdateWarehouseAreas/)
- assert.match(pageSource, /fetchDeleteWarehouseAreas/)
- assert.match(pageSource, /WarehouseAreasDetailDrawer/)
- assert.match(pageSource, /ArtSearchBar/)
- assert.match(pageSource, /ArtTable/)
-})
-
-test('backend menu adapter releases warehouse areas route and static route is registered', async () => {
- const backendMenuAdapterSource = await readFile(fileURLToPath(backendMenuAdapterPath), 'utf8')
- const staticRoutesSource = await readFile(fileURLToPath(staticRoutesPath), 'utf8')
-
- assert.match(backendMenuAdapterSource, /warehouseAreas:\s*'\/basic-info\/warehouse-areas'/)
- assert.match(staticRoutesSource, /path: 'warehouse-areas'/)
- assert.match(staticRoutesSource, /title:\s*'menu\.warehouseAreas'/)
-})
diff --git a/rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs b/rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs
deleted file mode 100644
index 77ce88b..0000000
--- a/rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs
+++ /dev/null
@@ -1,79 +0,0 @@
-import assert from 'node:assert/strict'
-import test from 'node:test'
-
-test('builds matnr page params with trimmed filters and group ids', async () => {
- const { buildMatnrPageQueryParams } = await import(
- '../src/views/basic-info/wh-mat/whMatPage.helpers.js'
- )
-
- assert.deepEqual(
- buildMatnrPageQueryParams({
- current: 2,
- pageSize: 30,
- condition: ' 鍗婃垚鍝� ',
- code: ' RM001 ',
- name: ' 鐗╂枡A ',
- spec: '',
- groupId: 19
- }),
- {
- current: 2,
- pageSize: 30,
- condition: '鍗婃垚鍝�',
- code: 'RM001',
- name: '鐗╂枡A',
- groupId: '19'
- }
- )
-})
-
-test('normalizes matnr group trees for el-tree rendering', async () => {
- const { normalizeMatnrGroupTreeRows } = await import(
- '../src/views/basic-info/wh-mat/whMatPage.helpers.js'
- )
-
- const tree = normalizeMatnrGroupTreeRows([
- {
- id: 1,
- parentId: 0,
- name: '鍗婃垚鍝�',
- code: 'RM',
- status: 1,
- children: [
- {
- id: 2,
- parentId: 1,
- name: '鍗婃垚鍝丄',
- code: 'RM-A',
- status: 0
- }
- ]
- }
- ])
-
- assert.equal(tree[0].displayLabel, '鍗婃垚鍝� 路 RM')
- assert.equal(tree[0].children[0].statusText, '鍐荤粨')
-})
-
-test('normalizes matnr detail fields for detail drawer display', async () => {
- const { normalizeMatnrDetail } = await import('../src/views/basic-info/wh-mat/whMatPage.helpers.js')
-
- const detail = normalizeMatnrDetail({
- id: 8,
- code: 'RM001',
- name: '鍗婃垚鍝�',
- groupId$: '鍘熸潗鏂�',
- status: 1,
- stockLeval$: ' A',
- flagLabelMange$: ' 鏄�',
- extendFields: {
- batch: 'B001'
- }
- })
-
- assert.equal(detail.code, 'RM001')
- assert.equal(detail.groupName, '鍘熸潗鏂�')
- assert.equal(detail.statusText, '姝e父')
- assert.equal(detail.extendFields.batch, 'B001')
-})
-
diff --git a/rsf-design/tests/clean-dev-helpers.test.mjs b/rsf-design/tests/clean-dev-helpers.test.mjs
deleted file mode 100644
index 1c536f7..0000000
--- a/rsf-design/tests/clean-dev-helpers.test.mjs
+++ /dev/null
@@ -1,112 +0,0 @@
-import assert from 'node:assert/strict'
-import test from 'node:test'
-
-import {
- CLEAN_DEV_TARGETS,
- buildMinimalRouteModuleFiles,
- createMinimalChangeLogContent,
- createMinimalFastEnterContent,
- pruneLocaleMessages,
- rewriteMenuApiContent
-} from '../scripts/clean-dev.helpers.mjs'
-
-test('clean:dev targets align with current JS workspace structure', () => {
- assert.ok(CLEAN_DEV_TARGETS.includes('src/views/template'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/views/widgets'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/views/examples'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/views/article'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/views/safeguard'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/views/change'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/views/dashboard/analysis'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/views/dashboard/ecommerce'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/components/core/charts/art-map-chart'))
- assert.ok(CLEAN_DEV_TARGETS.includes('src/components/business/comment-widget'))
-})
-
-test('minimal route module files keep only dashboard/system/result/exception', () => {
- const routeFiles = buildMinimalRouteModuleFiles()
-
- assert.match(routeFiles.dashboard, /path: 'console'/)
- assert.doesNotMatch(routeFiles.dashboard, /analysis/)
- assert.doesNotMatch(routeFiles.dashboard, /ecommerce/)
- assert.doesNotMatch(routeFiles.system, /nested/i)
- assert.match(routeFiles.index, /dashboardRoutes/)
- assert.match(routeFiles.index, /systemRoutes/)
- assert.match(routeFiles.index, /resultRoutes/)
- assert.match(routeFiles.index, /exceptionRoutes/)
- assert.doesNotMatch(
- routeFiles.index,
- /templateRoutes|widgetsRoutes|examplesRoutes|articleRoutes|helpRoutes/
- )
-})
-
-test('locale pruning removes demo menu entries and extra dashboard/system branches', () => {
- const locale = {
- menus: {
- dashboard: {
- title: 'Dashboard',
- console: 'Console',
- analysis: 'Analysis',
- ecommerce: 'Ecommerce'
- },
- widgets: { title: 'Widgets' },
- template: { title: 'Template' },
- article: { title: 'Article' },
- examples: { title: 'Examples' },
- safeguard: { title: 'Safeguard' },
- plan: { title: 'Plan' },
- help: { title: 'Help' },
- system: {
- title: 'System',
- user: 'User',
- role: 'Role',
- userCenter: 'User Center',
- menu: 'Menu',
- nested: 'Nested',
- menu1: 'Menu1',
- menu2: 'Menu2',
- menu21: 'Menu21',
- menu3: 'Menu3',
- menu31: 'Menu31',
- menu32: 'Menu32',
- menu321: 'Menu321'
- }
- }
- }
-
- const result = pruneLocaleMessages(locale)
-
- assert.deepEqual(Object.keys(result.menus).sort(), ['dashboard', 'system'])
- assert.deepEqual(Object.keys(result.menus.dashboard).sort(), ['console', 'title'])
- assert.deepEqual(Object.keys(result.menus.system).sort(), [
- 'menu',
- 'role',
- 'title',
- 'user',
- 'userCenter'
- ])
-})
-
-test('minimal fast enter content removes demo quick entries', () => {
- const content = createMinimalFastEnterContent()
-
- assert.match(content, /routeName: 'Console'/)
- assert.match(content, /routeName: 'Login'/)
- assert.match(content, /routeName: 'Register'/)
- assert.match(content, /routeName: 'ForgetPassword'/)
- assert.match(content, /routeName: 'UserCenter'/)
- assert.doesNotMatch(content, /Analysis|Fireworks|Chat|ChangeLog|Pricing|ArticleComment/)
-})
-
-test('menu api rewrite switches to simple endpoint', () => {
- const content = "url: '/api/v3/system/menus'"
-
- assert.equal(rewriteMenuApiContent(content), "url: '/api/v3/system/menus/simple'")
-})
-
-test('minimal change log content clears upgrade history', () => {
- assert.equal(
- createMinimalChangeLogContent().trim(),
- "import { ref } from 'vue'\n\nexport const upgradeLogList = ref([])"
- )
-})
diff --git a/rsf-design/tests/iconify-local-minimal.test.mjs b/rsf-design/tests/iconify-local-minimal.test.mjs
deleted file mode 100644
index b3925d5..0000000
--- a/rsf-design/tests/iconify-local-minimal.test.mjs
+++ /dev/null
@@ -1,112 +0,0 @@
-import assert from 'node:assert/strict'
-import fs from 'node:fs'
-import path from 'node:path'
-import test from 'node:test'
-import { fileURLToPath, pathToFileURL } from 'node:url'
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url))
-const projectRoot = path.resolve(__dirname, '..')
-const srcRoot = path.join(projectRoot, 'src')
-const iconRegistryModule = pathToFileURL(path.join(srcRoot, 'plugins', 'iconify.js')).href
-
-function collectSourceFiles(dir) {
- const entries = fs.readdirSync(dir, { withFileTypes: true })
- return entries.flatMap((entry) => {
- const fullPath = path.join(dir, entry.name)
-
- if (entry.isDirectory()) {
- return collectSourceFiles(fullPath)
- }
-
- return /\.(vue|js)$/.test(entry.name) ? [fullPath] : []
- })
-}
-
-function collectUsedIconsByPrefix() {
- const iconPattern = /["']([a-z0-9-]+):([a-z0-9-]+)["']/g
- const knownPrefixes = new Set([
- 'fluent',
- 'icon-park-outline',
- 'iconamoon',
- 'ix',
- 'line-md',
- 'ri',
- 'svg-spinners',
- 'system-uicons',
- 'vaadin'
- ])
- const usedIconsByPrefix = new Map()
-
- for (const filePath of collectSourceFiles(srcRoot)) {
- const content = fs.readFileSync(filePath, 'utf8')
-
- for (const [, prefix, name] of content.matchAll(iconPattern)) {
- if (!knownPrefixes.has(prefix)) {
- continue
- }
-
- const names = usedIconsByPrefix.get(prefix) || new Set()
- names.add(name)
- usedIconsByPrefix.set(prefix, names)
- }
- }
-
- return usedIconsByPrefix
-}
-
-function collectRequiredEntries(collection, usedNames) {
- const requiredIcons = new Set()
- const requiredAliases = new Set()
- const aliases = collection.aliases || {}
-
- function addDependencies(name) {
- if (collection.icons[name]) {
- requiredIcons.add(name)
- return
- }
-
- const alias = aliases[name]
- assert.ok(alias, `Icon "${collection.prefix}:${name}" is missing from local collection`)
-
- if (requiredAliases.has(name)) {
- return
- }
-
- requiredAliases.add(name)
- addDependencies(alias.parent)
- }
-
- usedNames.forEach((name) => addDependencies(name))
-
- return {
- icons: [...requiredIcons].sort(),
- aliases: [...requiredAliases].sort()
- }
-}
-
-test('local Iconify collections only contain icons that are actually used by the app', async () => {
- const { LOCAL_ICON_COLLECTIONS } = await import(iconRegistryModule)
- const usedIconsByPrefix = collectUsedIconsByPrefix()
-
- for (const [prefix, usedNames] of usedIconsByPrefix.entries()) {
- const collection = LOCAL_ICON_COLLECTIONS[prefix]
-
- assert.ok(collection, `Missing local collection for prefix "${prefix}"`)
-
- const requiredEntries = collectRequiredEntries(collection, usedNames)
- const actualIcons = Object.keys(collection.icons || {}).sort()
- const actualAliases = Object.keys(collection.aliases || {}).sort()
-
- assert.deepEqual(
- actualIcons,
- requiredEntries.icons,
- `Local collection "${prefix}" includes unused icon entries`
- )
-
- assert.deepEqual(
- actualAliases,
- requiredEntries.aliases,
- `Local collection "${prefix}" includes unused alias entries`
- )
- }
-})
diff --git a/rsf-design/tests/iconify-local-prefixes.test.mjs b/rsf-design/tests/iconify-local-prefixes.test.mjs
deleted file mode 100644
index 7041182..0000000
--- a/rsf-design/tests/iconify-local-prefixes.test.mjs
+++ /dev/null
@@ -1,65 +0,0 @@
-import assert from 'node:assert/strict'
-import test from 'node:test'
-import fs from 'node:fs'
-import path from 'node:path'
-import { fileURLToPath, pathToFileURL } from 'node:url'
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url))
-const projectRoot = path.resolve(__dirname, '..')
-const srcRoot = path.join(projectRoot, 'src')
-const iconRegistryModule = pathToFileURL(path.join(srcRoot, 'plugins', 'iconify.js')).href
-
-function collectSourceFiles(dir) {
- const entries = fs.readdirSync(dir, { withFileTypes: true })
- return entries.flatMap((entry) => {
- const fullPath = path.join(dir, entry.name)
-
- if (entry.isDirectory()) {
- return collectSourceFiles(fullPath)
- }
-
- return /\.(vue|js)$/.test(entry.name) ? [fullPath] : []
- })
-}
-
-function collectIconPrefixes() {
- const iconPattern = /["']([a-z0-9-]+):([a-z0-9-]+)["']/g
- const knownPrefixes = new Set([
- 'fluent',
- 'icon-park-outline',
- 'iconamoon',
- 'ix',
- 'line-md',
- 'ri',
- 'svg-spinners',
- 'system-uicons',
- 'vaadin'
- ])
- const prefixes = new Set()
-
- for (const filePath of collectSourceFiles(srcRoot)) {
- const content = fs.readFileSync(filePath, 'utf8')
-
- for (const [, prefix] of content.matchAll(iconPattern)) {
- if (!knownPrefixes.has(prefix)) {
- continue
- }
-
- prefixes.add(prefix)
- }
- }
-
- return [...prefixes].sort()
-}
-
-test('all used Iconify prefixes are mapped to local collections', async () => {
- const { LOCAL_ICON_COLLECTIONS } = await import(iconRegistryModule)
- const actualPrefixes = collectIconPrefixes()
- const registeredPrefixes = Object.keys(LOCAL_ICON_COLLECTIONS).sort()
-
- assert.deepEqual(
- registeredPrefixes,
- actualPrefixes,
- `Missing local Iconify collections for: ${actualPrefixes.filter((prefix) => !registeredPrefixes.includes(prefix)).join(', ')}`
- )
-})
diff --git a/rsf-design/tests/list-export-print-contract.test.mjs b/rsf-design/tests/list-export-print-contract.test.mjs
deleted file mode 100644
index d7a2f50..0000000
--- a/rsf-design/tests/list-export-print-contract.test.mjs
+++ /dev/null
@@ -1,103 +0,0 @@
-import assert from 'node:assert/strict'
-import test from 'node:test'
-
-import {
- buildListExportPayload,
- buildPrintPageQuery,
- toExportColumns
-} from '../src/components/biz/list-export-print/list-export-print.helpers.js'
-
-test('selected rows keep export payload on ids only instead of flat query params', () => {
- const payload = buildListExportPayload({
- reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
- selectedRows: [{ id: 7 }, { id: 9 }],
- queryParams: { current: 1, pageSize: 20, name: '绠$悊鍛�', code: 'R_ADMIN' },
- columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О' }]
- })
-
- assert.deepEqual(payload.ids, [7, 9])
- assert.deepEqual(payload.columns, [{ source: 'name', label: '瑙掕壊鍚嶇О' }])
- assert.equal(payload.meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
- assert.deepEqual(
- Object.keys(payload).filter((key) => ['current', 'pageSize', 'name', 'code', 'queryParams'].includes(key)),
- []
- )
-})
-
-test('export columns use ListExportService source/label contract only', () => {
- assert.deepEqual(
- toExportColumns([
- { prop: 'name', label: '瑙掕壊鍚嶇О' },
- { prop: 'operation', label: '鎿嶄綔' },
- { prop: 'selection', label: '鍕鹃��' }
- ]),
- [{ source: 'name', label: '瑙掕壊鍚嶇О' }]
- )
-})
-
-test('export payload keeps no-filter searches legal as flat params', () => {
- const payload = buildListExportPayload({
- reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
- selectedRows: [],
- queryParams: { current: 1, pageSize: 20 },
- columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О' }]
- })
-
- assert.equal(payload.current, 1)
- assert.equal(payload.pageSize, 20)
- assert.equal(payload.meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
- assert.deepEqual(payload.ids, [])
- assert.equal(Object.prototype.hasOwnProperty.call(payload, 'queryParams'), false)
-})
-
-test('export payload preserves report style meta and column align without top-level reportStyle', () => {
- const payload = buildListExportPayload({
- reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
- meta: {
- reportStyle: {
- orientation: 'landscape',
- density: 'comfortable'
- }
- },
- columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О', align: 'right' }]
- })
-
- assert.deepEqual(payload.meta.reportStyle, {
- orientation: 'landscape',
- density: 'comfortable'
- })
- assert.equal(payload.columns[0].align, 'right')
- assert.equal(Object.prototype.hasOwnProperty.call(payload, 'reportStyle'), false)
-})
-
-test('print query expands to the full result set instead of the current page size', () => {
- assert.deepEqual(
- buildPrintPageQuery({
- queryParams: { current: 3, pageSize: 20, orderBy: 'createTime desc', name: '绠$悊鍛�' },
- total: 86,
- maxResults: 1000
- }),
- {
- current: 1,
- pageSize: 86,
- orderBy: 'createTime desc',
- name: '绠$悊鍛�'
- }
- )
-})
-
-test('print query caps pageSize at maxResults when total is larger', () => {
- assert.deepEqual(
- buildPrintPageQuery({
- queryParams: { current: 5, pageSize: 20, orderBy: 'createTime desc', code: 'R_ADMIN' },
- total: 1500,
- maxResults: 1000
- }),
- {
- current: 1,
- pageSize: 1000,
- orderBy: 'createTime desc',
- code: 'R_ADMIN'
- }
- )
-})
diff --git a/rsf-design/tests/list-print-preview-style-contract.test.mjs b/rsf-design/tests/list-print-preview-style-contract.test.mjs
deleted file mode 100644
index 760c0e1..0000000
--- a/rsf-design/tests/list-print-preview-style-contract.test.mjs
+++ /dev/null
@@ -1,202 +0,0 @@
-import assert from 'node:assert/strict'
-import { readFileSync } from 'node:fs'
-import test from 'node:test'
-
-import {
- buildPreviewColumns,
- buildReportStyleMeta
-} from '../src/components/biz/list-export-print/list-export-print.helpers.js'
-import { buildPrintDocumentHtml } from '../src/components/biz/list-export-print/list-print-document.js'
-
-const previewDialogSource = readFileSync(
- new URL('../src/components/biz/list-export-print/list-print-preview-dialog.vue', import.meta.url),
- 'utf8'
-)
-const printDocumentSource = readFileSync(
- new URL('../src/components/biz/list-export-print/list-print-document.js', import.meta.url),
- 'utf8'
-)
-
-const scriptSetupIndex = previewDialogSource.indexOf('<script setup>')
-const templateBlock =
- scriptSetupIndex >= 0
- ? previewDialogSource.slice(previewDialogSource.indexOf('<template>'), scriptSetupIndex)
- : previewDialogSource
-
-test('report style meta defaults to the shared print preview contract', () => {
- assert.deepEqual(buildReportStyleMeta({ reportTitle: '瑙掕壊鎶ヨ〃' }).reportStyle, {
- titleAlign: 'center',
- titleLevel: 'strong',
- orientation: 'portrait',
- density: 'compact',
- showSequence: true,
- showBorder: true
- })
-})
-
-test('preview columns do not inject a sequence column when disabled', () => {
- assert.deepEqual(
- buildPreviewColumns({
- columns: [{ source: 'name', label: '瑙掕壊鍚嶇О' }],
- reportStyle: { showSequence: false }
- }),
- [{ source: 'name', label: '瑙掕壊鍚嶇О' }]
- )
-})
-
-test('preview dialog template uses passed columns for sequence rendering and colspan', () => {
- assert.equal(templateBlock.includes('>搴忓彿<'), false)
- assert.match(templateBlock, /<div\s+:class="titleClass">\s*\{\{\s*meta\.reportTitle\s*\?\?\s*'--'\s*\}\}\s*<\/div>/)
- assert.match(templateBlock, /<div\s+v-for="item in metaItems"[\s\S]*?:key="item\.key"[\s\S]*?>/)
- assert.match(templateBlock, /\{\{\s*item\.label\s*\}\}/)
- assert.match(templateBlock, /\{\{\s*item\.value\s*\?\?\s*'--'\s*\}\}/)
- assert.match(templateBlock, /<th[\s\S]*?v-for="column in columns"[\s\S]*?>[\s\S]*?\{\{\s*column\.label\s*\}\}[\s\S]*?<\/th>/)
- assert.match(
- templateBlock,
- /<td[\s\S]*?v-for="column in columns"[\s\S]*?>[\s\S]*?\{\{\s*column\.source === '__sequence__' \? index \+ 1 : row\?\.\[column\.source\] \?\? '--'\s*\}\}[\s\S]*?<\/td>/
- )
- assert.ok(templateBlock.includes("column.source === '__sequence__' ? index + 1"))
- assert.ok(templateBlock.includes('Math.max(columns.length, 1)'))
- assert.ok(previewDialogSource.includes('鎶ヨ〃鏃ユ湡'))
- assert.ok(previewDialogSource.includes('鎵撳嵃浜�'))
- assert.ok(previewDialogSource.includes('鎵撳嵃鏃堕棿'))
- assert.ok(previewDialogSource.includes('璁板綍鏁�'))
- assert.ok(templateBlock.includes('border-b'))
- assert.ok(templateBlock.includes('print:hidden'))
-})
-
-test('preview dialog derives titleClass from reportStyle title alignment and level', () => {
- assert.ok(previewDialogSource.includes('const titleClass = computed(() =>'))
- assert.ok(previewDialogSource.includes('meta.reportTitle'))
- assert.ok(previewDialogSource.includes('titleClass'))
- assert.ok(previewDialogSource.includes('reportStyle.titleAlign'))
- assert.ok(previewDialogSource.includes('reportStyle.titleLevel'))
- assert.ok(previewDialogSource.includes("left: 'text-left'"))
- assert.ok(previewDialogSource.includes("center: 'text-center'"))
- assert.ok(previewDialogSource.includes("right: 'text-right'"))
- assert.ok(previewDialogSource.includes("normal: 'text-[18px] font-medium'"))
- assert.ok(previewDialogSource.includes("strong: 'text-[22px] font-semibold'"))
- assert.ok(previewDialogSource.includes("prominent: 'text-[26px] font-bold'"))
- assert.ok(previewDialogSource.includes("?? alignMap.center"))
- assert.ok(previewDialogSource.includes("?? levelMap.strong"))
-})
-
-test('preview dialog keeps report content compact and prioritizes showing all columns', () => {
- assert.ok(previewDialogSource.includes("text-[18px] font-medium"))
- assert.ok(previewDialogSource.includes("text-[22px] font-semibold"))
- assert.ok(previewDialogSource.includes("text-[26px] font-bold"))
- assert.ok(previewDialogSource.includes('grid grid-cols-4 gap-2 text-[11px]'))
- assert.ok(previewDialogSource.includes('table-auto'))
- assert.ok(previewDialogSource.includes('text-[11px] leading-tight'))
- assert.ok(previewDialogSource.includes('px-1.5 py-1.5'))
- assert.ok(previewDialogSource.includes('break-all'))
-})
-
-test('preview dialog prints through a standalone report document instead of window.print on the app shell', () => {
- assert.equal(previewDialogSource.includes('window.print()'), false)
- assert.ok(previewDialogSource.includes("import { printReportDocument } from './list-print-document.js'"))
- assert.ok(previewDialogSource.includes('printReportDocument({'))
-})
-
-test('standalone print document preserves report title, meta row, sequence column, and print page styles', () => {
- const html = buildPrintDocumentHtml({
- title: '瑙掕壊绠$悊鎶ヨ〃',
- meta: {
- reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
- reportDate: '2026/3/29',
- operator: 'root',
- printedAt: '2026/3/29 17:31:00',
- count: 2,
- reportStyle: {
- titleAlign: 'center',
- titleLevel: 'strong',
- orientation: 'portrait'
- }
- },
- columns: [
- { source: '__sequence__', label: '搴忓彿', align: 'center' },
- { source: 'name', label: '瑙掕壊鍚嶇О' },
- { source: 'code', label: '瑙掕壊缂栫爜' }
- ],
- rows: [
- { id: 1, name: 'WMS绯荤粺绠$悊鍛�', code: 'admin' },
- { id: 2, name: 'ERP璐㈠姟绠$悊鍛�', code: 'erpcw' }
- ]
- })
-
- assert.ok(html.includes('<title>瑙掕壊绠$悊鎶ヨ〃</title>'))
- assert.ok(html.includes('鎶ヨ〃鏃ユ湡'))
- assert.ok(html.includes('鎵撳嵃浜�'))
- assert.ok(html.includes('鎵撳嵃鏃堕棿'))
- assert.ok(html.includes('璁板綍鏁�'))
- assert.ok(html.includes('搴忓彿'))
- assert.ok(html.includes('WMS绯荤粺绠$悊鍛�'))
- assert.ok(html.includes('ERP璐㈠姟绠$悊鍛�'))
- assert.ok(html.includes('@page'))
- assert.ok(html.includes('size: A4 portrait;'))
- const pageRule = html.match(/@page\s*\{[\s\S]*?\}/)?.[0] ?? ''
- assert.equal(pageRule.includes('margin:'), false)
- assert.ok(html.includes('print-color-adjust: exact;'))
- assert.ok(html.includes('window.print()'))
- assert.ok(html.includes('window.close()'))
- assert.ok(html.includes('font-size: 11px;'))
- assert.ok(html.includes('font-size: 22px;'))
- assert.ok(html.includes('table-layout: auto;'))
-})
-
-test('standalone print document opens a writable popup window for document injection', () => {
- assert.equal(printDocumentSource.includes('noopener,noreferrer'), false)
- assert.ok(printDocumentSource.includes("window.open('', '_blank')"))
- assert.ok(printDocumentSource.includes('printWindow.document.write(buildPrintDocumentHtml(payload))'))
-})
-
-test('preview dialog caps rendered rows and keeps the table body scrollable for large datasets', () => {
- assert.ok(previewDialogSource.includes("maxPreviewRows: { type: Number, default: 50 }"))
- assert.ok(previewDialogSource.includes('const previewRows = computed(() =>'))
- assert.ok(previewDialogSource.includes('props.rows.slice(0, props.maxPreviewRows)'))
- assert.ok(previewDialogSource.includes('const hiddenRowCount = computed(() =>'))
- assert.ok(previewDialogSource.includes('棰勮浠呭睍绀哄墠'))
- assert.ok(previewDialogSource.includes('min-h-0 flex-1 overflow-auto'))
- assert.ok(previewDialogSource.includes('v-for="(row, index) in previewRows"'))
- assert.ok(previewDialogSource.includes('v-if="hiddenRowCount > 0"'))
-})
-
-test('preview dialog keeps the whole modal inside the viewport and lets only the table area scroll', () => {
- assert.ok(previewDialogSource.includes('width="min(96vw, 1100px)"'))
- assert.ok(previewDialogSource.includes('top="4vh"'))
- assert.ok(previewDialogSource.includes('class="max-h-[88vh] overflow-hidden"'))
- assert.ok(previewDialogSource.includes('flex max-h-[calc(88vh-160px)] min-h-0 flex-col'))
- assert.ok(previewDialogSource.includes('mx-auto flex min-h-0 flex-1 flex-col overflow-hidden'))
- assert.ok(previewDialogSource.includes(':class="paperClass"'))
- assert.ok(previewDialogSource.includes(':style="paperStyle"'))
- assert.ok(previewDialogSource.includes('mt-4 min-h-0 flex-1 overflow-auto'))
-})
-
-test('preview dialog lets the user switch between portrait and landscape before printing', () => {
- assert.ok(previewDialogSource.includes("const currentOrientation = ref('portrait')"))
- assert.ok(previewDialogSource.includes('watch('))
- assert.ok(previewDialogSource.includes('v-model="currentOrientation"'))
- assert.ok(previewDialogSource.includes('label="portrait"'))
- assert.ok(previewDialogSource.includes('label="landscape"'))
- assert.ok(previewDialogSource.includes('绔栫増'))
- assert.ok(previewDialogSource.includes('妯増'))
- assert.ok(previewDialogSource.includes('const effectiveMeta = computed(() =>'))
- assert.ok(previewDialogSource.includes('orientation: currentOrientation.value'))
- assert.ok(previewDialogSource.includes('meta: effectiveMeta.value'))
- assert.ok(previewDialogSource.includes('const paperClass = computed(() =>'))
- assert.ok(previewDialogSource.includes("currentOrientation.value === 'landscape'"))
- assert.ok(previewDialogSource.includes("aspectRatio: currentOrientation.value === 'landscape' ? '297 / 210' : '210 / 297'"))
-})
-
-test('preview dialog lets the user toggle table borders and carries the setting into print meta', () => {
- assert.ok(previewDialogSource.includes("const currentShowBorder = ref(true)"))
- assert.ok(previewDialogSource.includes('v-model="currentShowBorder"'))
- assert.ok(previewDialogSource.includes('杈规寮�'))
- assert.ok(previewDialogSource.includes('杈规鍏�'))
- assert.ok(previewDialogSource.includes('showBorder: currentShowBorder.value'))
- assert.ok(previewDialogSource.includes('const tableWrapClass = computed(() =>'))
- assert.ok(previewDialogSource.includes('const headerCellClass = computed(() =>'))
- assert.ok(previewDialogSource.includes('const bodyCellClass = computed(() =>'))
- assert.ok(printDocumentSource.includes('const showBorder = reportStyle.showBorder !== false'))
- assert.ok(printDocumentSource.includes("const tableWrapClass = showBorder ? 'report-table-wrap' : 'report-table-wrap report-table-wrap-borderless'"))
-})
diff --git a/rsf-design/tests/manual-chunks.test.mjs b/rsf-design/tests/manual-chunks.test.mjs
deleted file mode 100644
index a4f2fd1..0000000
--- a/rsf-design/tests/manual-chunks.test.mjs
+++ /dev/null
@@ -1,37 +0,0 @@
-import assert from 'node:assert/strict'
-import test from 'node:test'
-
-import { createManualChunks } from '../build/manualChunks.js'
-
-test('createManualChunks groups heavy dependencies into stable vendor chunks', () => {
- assert.equal(createManualChunks('/repo/node_modules/echarts/core.js'), 'vendor-echarts')
- assert.equal(
- createManualChunks('/repo/node_modules/@wangeditor/editor-for-vue/dist/index.js'),
- 'vendor-editor'
- )
- assert.equal(createManualChunks('/repo/node_modules/highlight.js/lib/index.js'), 'vendor-editor')
- assert.equal(createManualChunks('/repo/node_modules/xlsx/xlsx.mjs'), 'vendor-xlsx')
- assert.equal(createManualChunks('/repo/node_modules/xgplayer/dist/index.min.js'), 'vendor-media')
- assert.equal(
- createManualChunks('/repo/node_modules/element-plus/es/index.mjs'),
- 'vendor-element-plus'
- )
- assert.equal(
- createManualChunks('/repo/node_modules/@element-plus/icons-vue/dist/index.mjs'),
- 'vendor-element-plus'
- )
- assert.equal(createManualChunks('/repo/node_modules/vue-router/dist/index.mjs'), 'vendor-vue')
- assert.equal(createManualChunks('/repo/node_modules/pinia/dist/pinia.mjs'), 'vendor-vue')
- assert.equal(createManualChunks('/repo/node_modules/@vueuse/core/index.mjs'), 'vendor-vue')
- assert.equal(createManualChunks('/repo/node_modules/@iconify/vue/dist/index.mjs'), 'vendor-utils')
- assert.equal(
- createManualChunks('/repo/node_modules/file-saver/dist/FileSaver.min.js'),
- 'vendor-utils'
- )
- assert.equal(createManualChunks('/repo/node_modules/axios/index.js'), 'vendor-utils')
-})
-
-test('createManualChunks leaves application modules untouched', () => {
- assert.equal(createManualChunks('/repo/src/views/dashboard/index.vue'), undefined)
- assert.equal(createManualChunks('\u0000plugin-vue:export-helper'), undefined)
-})
diff --git a/rsf-design/tests/navigation-home-path.test.mjs b/rsf-design/tests/navigation-home-path.test.mjs
deleted file mode 100644
index ad5e873..0000000
--- a/rsf-design/tests/navigation-home-path.test.mjs
+++ /dev/null
@@ -1,65 +0,0 @@
-import test from 'node:test'
-import assert from 'node:assert/strict'
-
-test('getFirstMenuPath skips unreleased menu routes and selects the first implemented page', async () => {
- const { getFirstMenuPath } = await import('../src/utils/navigation/route.js')
-
- const menuList = [
- {
- path: '/system',
- children: [
- {
- path: '/system/aiParam',
- component: '/system/aiParam',
- meta: { title: 'AI 鍙傛暟' }
- }
- ]
- },
- {
- path: '/logs',
- children: [
- {
- path: '/logs/system/userLogin',
- component: '/system/user-login',
- meta: { title: '鐧诲綍鏃ュ織' }
- }
- ]
- }
- ]
-
- assert.equal(getFirstMenuPath(menuList), '/logs/system/userLogin')
-})
-
-test('getFirstMenuPath keeps traversing siblings when the first branch has no implemented leaf', async () => {
- const { getFirstMenuPath } = await import('../src/utils/navigation/route.js')
-
- const menuList = [
- {
- path: '/ai',
- children: [
- {
- path: '/ai/param',
- component: '/system/aiParam',
- meta: { title: 'AI 鍙傛暟' }
- }
- ]
- },
- {
- path: '/permissions',
- children: [
- {
- path: '/permissions/system',
- children: [
- {
- path: '/permissions/system/role',
- component: '/system/role',
- meta: { title: '瑙掕壊绠$悊' }
- }
- ]
- }
- ]
- }
- ]
-
- assert.equal(getFirstMenuPath(menuList), '/permissions/system/role')
-})
diff --git a/rsf-design/tests/repo-hygiene.test.mjs b/rsf-design/tests/repo-hygiene.test.mjs
deleted file mode 100644
index c7ac397..0000000
--- a/rsf-design/tests/repo-hygiene.test.mjs
+++ /dev/null
@@ -1,37 +0,0 @@
-import assert from 'node:assert/strict'
-import fs from 'node:fs'
-import path from 'node:path'
-import test from 'node:test'
-
-const projectRoot = path.resolve(import.meta.dirname, '..')
-
-const migrationArtifacts = [
- 'docs/superpowers/specs/2026-03-27-art-design-pro-js-migration-design.md',
- 'docs/superpowers/plans/2026-03-27-art-design-pro-js-migration-plan.md',
- 'scripts/migrate-to-js.mjs',
- 'scripts/restore-vue-template-imports.mjs',
- 'tests/js-migration-sanity.test.mjs',
- 'src/types'
-]
-
-test('migration-only artifacts have been removed from the working tree', () => {
- for (const relativePath of migrationArtifacts) {
- const fullPath = path.join(projectRoot, relativePath)
- assert.equal(
- fs.existsSync(fullPath),
- false,
- `Expected migration-only artifact to be removed: ${relativePath}`
- )
- }
-})
-
-test('package scripts no longer expose one-off migration entry points', () => {
- const packageJsonPath = path.join(projectRoot, 'package.json')
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
-
- assert.equal(
- Reflect.has(packageJson.scripts || {}, 'migrate:js'),
- false,
- 'Expected package.json scripts.migrate:js to be removed'
- )
-})
diff --git a/rsf-design/tests/system-manage-contract.test.mjs b/rsf-design/tests/system-manage-contract.test.mjs
deleted file mode 100644
index f542a6b..0000000
--- a/rsf-design/tests/system-manage-contract.test.mjs
+++ /dev/null
@@ -1,147 +0,0 @@
-import assert from 'node:assert/strict'
-import { register } from 'node:module'
-import test from 'node:test'
-
-const menuTreeFixture = [
- {
- id: 1,
- parentId: 0,
- name: 'menu.user',
- route: 'user',
- type: 0,
- component: 'user',
- authority: 'system:user',
- icon: 'People',
- sort: 1,
- status: 1,
- memo: 'User menu',
- children: [
- {
- id: 2,
- parentId: 1,
- name: 'Query User',
- type: 1,
- authority: 'system:user:list',
- icon: 'Search',
- sort: 1,
- status: 0,
- memo: 'Query action'
- }
- ]
- }
-]
-
-register(
- `data:text/javascript,export function resolve(specifier, context, nextResolve){ if(specifier===\'@/utils/http\'){ return { shortCircuit:true, url:\'data:text/javascript,export default { get(){ return Promise.resolve({}) }, post(config){ if(config?.url===\\'/menu/list\\'){ globalThis.__lastMenuListConfig = config; return Promise.resolve(${JSON.stringify(menuTreeFixture.flatMap((node) => [node, ...node.children]))}) } if(config?.url===\\'/menu/tree\\'){ globalThis.__lastMenuTreeConfig = config; return Promise.resolve(${JSON.stringify(menuTreeFixture)}) } return Promise.resolve(config) } }\' } } return nextResolve(specifier, context) }`,
- import.meta.url
-)
-
-const {
- buildUserListParams,
- buildRoleListParams,
- fetchDeleteMenu,
- fetchGetMenuList,
- fetchGetMenuTree,
- fetchResetUserPassword,
- fetchGetRoleScopeList,
- fetchGetRoleScopeTree,
- fetchSaveMenu,
- fetchUpdateMenu
-} = await import('../src/api/system-manage.js')
-
-test('buildUserListParams matches the rsf-admin paging contract', () => {
- assert.equal(typeof buildUserListParams, 'function')
- assert.equal(typeof buildRoleListParams, 'function')
-
- assert.deepEqual(
- buildUserListParams({
- current: 2,
- pageSize: 20,
- username: 'root',
- email: 'root@example.com',
- deptId: 3
- }),
- {
- current: 2,
- pageSize: 20,
- username: 'root',
- email: 'root@example.com',
- deptId: 3
- }
- )
-})
-
-test('fetchResetUserPassword requires an admin update payload', () => {
- assert.throws(() => fetchResetUserPassword(1), /object payload/i)
- assert.throws(() => fetchResetUserPassword({ id: 1 }), /password/i)
-
- return fetchResetUserPassword({ id: 1, newPassword: 'secret' }).then((config) => {
- assert.equal(config.params.password, 'secret')
- })
-})
-
-test('fetchGetMenuList folds button nodes into authList', async () => {
- const menuList = await fetchGetMenuList()
-
- assert.equal(menuList.length, 1)
- assert.equal(menuList[0].id, '1')
- assert.equal(menuList[0].parentId, '0')
- assert.equal(menuList[0].route, 'user')
- assert.equal(menuList[0].authority, 'system:user')
- assert.equal(menuList[0].status, 1)
- assert.equal(menuList[0].memo, 'User menu')
- assert.equal(menuList[0].meta.title, 'menus.user')
- assert.equal(menuList[0].component, 'user')
- assert.equal(menuList[0].meta.sort, 1)
- assert.equal(menuList[0].meta.isEnable, true)
- assert.deepEqual(menuList[0].meta.authList, [
- {
- id: '2',
- parentId: '1',
- parentName: '',
- name: 'Query User',
- title: 'Query User',
- route: '',
- component: '',
- authMark: 'system:user:list',
- authority: 'system:user:list',
- icon: 'Search',
- sort: 1,
- status: 0,
- memo: 'Query action',
- type: 1
- }
- ])
- assert.equal(menuList[0].children.length, 0)
- assert.deepEqual(globalThis.__lastMenuListConfig?.params, {})
-})
-
-test('menu tree helpers always send an explicit empty body for Spring Map request bodies', async () => {
- await fetchGetMenuTree()
- assert.deepEqual(globalThis.__lastMenuTreeConfig?.params, {})
-
- await fetchGetRoleScopeTree('menu')
- assert.equal(globalThis.__lastMenuTreeConfig?.url, '/menu/tree')
- assert.deepEqual(globalThis.__lastMenuTreeConfig?.params, {})
-})
-
-test('menu CRUD helpers target the real rsf-server endpoints', async () => {
- assert.equal(typeof fetchSaveMenu, 'function')
- assert.equal(typeof fetchUpdateMenu, 'function')
- assert.equal(typeof fetchDeleteMenu, 'function')
-
- const saveConfig = await fetchSaveMenu({ name: '鑿滃崟' })
- const updateConfig = await fetchUpdateMenu({ id: 1, name: '鑿滃崟' })
- const deleteConfig = await fetchDeleteMenu('1,2')
-
- assert.equal(saveConfig.url, '/menu/save')
- assert.deepEqual(saveConfig.params, { name: '鑿滃崟' })
- assert.equal(updateConfig.url, '/menu/update')
- assert.deepEqual(updateConfig.params, { id: 1, name: '鑿滃崟' })
- assert.equal(deleteConfig.url, '/menu/remove/1,2')
-})
-
-test('scope resolvers fail fast on invalid scope types', () => {
- assert.throws(() => fetchGetRoleScopeList('invalid', 1), /Unsupported scope type/i)
- assert.throws(() => fetchGetRoleScopeTree('invalid', {}), /Unsupported scope type/i)
-})
diff --git a/rsf-design/tests/system-menu-page-contract.test.mjs b/rsf-design/tests/system-menu-page-contract.test.mjs
deleted file mode 100644
index 4592ee2..0000000
--- a/rsf-design/tests/system-menu-page-contract.test.mjs
+++ /dev/null
@@ -1,56 +0,0 @@
-import assert from 'node:assert/strict'
-import { readFileSync } from 'node:fs'
-import test from 'node:test'
-
-const menuPageSource = readFileSync(
- new URL('../src/views/system/menu/index.vue', import.meta.url),
- 'utf8'
-)
-
-const menuDialogSource = readFileSync(
- new URL('../src/views/system/menu/modules/menu-dialog.vue', import.meta.url),
- 'utf8'
-)
-
-const routerSource = readFileSync(new URL('../src/utils/router.js', import.meta.url), 'utf8')
-const zhLocale = JSON.parse(readFileSync(new URL('../src/locales/langs/zh.json', import.meta.url), 'utf8'))
-const enLocale = JSON.parse(readFileSync(new URL('../src/locales/langs/en.json', import.meta.url), 'utf8'))
-
-test('menu page submit and delete handlers call the real backend menu APIs', () => {
- assert.match(menuPageSource, /fetchSaveMenu/)
- assert.match(menuPageSource, /fetchUpdateMenu/)
- assert.match(menuPageSource, /fetchDeleteMenu/)
- assert.doesNotMatch(menuPageSource, /console\.log\('鎻愪氦鏁版嵁:'/)
- assert.match(menuPageSource, /const handleAddAuth = \(row\) =>/)
- assert.match(menuPageSource, /const handleDeleteMenu = async \(row\) =>/)
- assert.match(menuPageSource, /const handleDeleteAuth = async \(row\) =>/)
- assert.match(menuPageSource, /await fetchDeleteMenu\(row\.id\)/)
-})
-
-test('menu dialog accepts edit data and parent menu options from the page', () => {
- assert.match(menuDialogSource, /editData:/)
- assert.match(menuDialogSource, /menuTreeOptions:/)
- assert.match(menuDialogSource, /key: 'parentId'/)
- assert.match(menuDialogSource, /type: 'treeselect'/)
-})
-
-test('menu page renders Iconify preview and current-system translated menu names', () => {
- assert.ok(
- menuPageSource.indexOf("label: '鑿滃崟鍚嶇О'") < menuPageSource.indexOf("label: '鍥炬爣棰勮'"),
- '鑿滃崟鍚嶇О鍒楀簲淇濇寔鍦ㄥ浘鏍囬瑙堝垪涔嬪墠锛岄伩鍏嶆爲褰㈠睍寮�绠ご璺戝埌鍥炬爣鍒�'
- )
- assert.match(menuPageSource, /label:\s*'鍥炬爣棰勮'/)
- assert.match(menuPageSource, /ArtSvgIcon/)
- assert.match(menuPageSource, /rounded-md/)
- assert.match(menuPageSource, /row\.meta\?\.title\s*\|\|\s*row\.name/)
- assert.match(routerSource, /startsWith\('menu\.'\)|startsWith\("menu\."\)/)
- assert.match(routerSource, /const fallbackTitle =/)
- assert.match(routerSource, /title\.startsWith\('menus\.'\)/)
- assert.match(routerSource, /i18n\.global\.te\(fallbackTitle\)/)
- assert.equal(zhLocale.menu?.system, '绯荤粺璁剧疆')
- assert.equal(zhLocale.menu?.basicInfo, '鍩虹淇℃伅')
- assert.equal(zhLocale.menu?.aiParam, 'AI 鍙傛暟')
- assert.equal(enLocale.menu?.system, 'System')
- assert.equal(enLocale.menu?.basicInfo, 'BasicInfo')
- assert.equal(enLocale.menu?.aiParam, 'AI Params')
-})
diff --git a/rsf-design/tests/system-role-print-export-page.test.mjs b/rsf-design/tests/system-role-print-export-page.test.mjs
deleted file mode 100644
index 3b557c6..0000000
--- a/rsf-design/tests/system-role-print-export-page.test.mjs
+++ /dev/null
@@ -1,97 +0,0 @@
-import assert from 'node:assert/strict'
-import { readFileSync } from 'node:fs'
-import test from 'node:test'
-
-import * as roleHelpers from '../src/views/system/role/rolePage.helpers.js'
-
-test('role report columns are the fixed business columns, not table utility columns', () => {
- assert.deepEqual(
- roleHelpers.getRoleReportColumns().map((column) => column.source),
- ['name', 'code', 'statusText', 'memo', 'createTimeText', 'updateTimeText']
- )
-})
-
-test('report columns keep visible order inside the role allowlist', () => {
- assert.deepEqual(
- roleHelpers.resolveRoleReportColumns([
- { prop: 'selection', label: '鍕鹃��' },
- { prop: 'status', label: '鐘舵��' },
- { prop: 'name', label: '瑙掕壊鍚嶇О' },
- { prop: 'deptName', label: '閮ㄩ棬鍚嶇О' },
- { prop: 'operation', label: '鎿嶄綔' },
- { prop: 'memo', label: '澶囨敞' }
- ]),
- [
- { source: 'statusText', label: '鐘舵��' },
- { source: 'name', label: '瑙掕壊鍚嶇О' },
- { source: 'memo', label: '澶囨敞' }
- ]
- )
-})
-
-test('role print rows expose formatted status text', () => {
- const rows = roleHelpers.buildRolePrintRows([
- { name: '绠$悊鍛�', status: 1 },
- { name: '璁垮', status: 0 }
- ])
-
- assert.equal(rows[0].statusText, '姝e父')
- assert.equal(rows[1].statusText, '绂佺敤')
-})
-
-test('role report meta applies shared report style and title', () => {
- const meta = roleHelpers.buildRoleReportMeta({
- previewMeta: {
- reportDate: '2026-03-29',
- printedAt: '2026-03-29 10:57:25',
- operator: 'ROOT'
- },
- count: 2,
- titleAlign: 'left',
- titleLevel: 'prominent'
- })
-
- assert.equal(meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
- assert.equal(meta.reportDate, '2026-03-29')
- assert.equal(meta.operator, 'ROOT')
- assert.deepEqual(meta.reportStyle, {
- titleAlign: 'left',
- titleLevel: 'prominent',
- orientation: 'portrait',
- density: 'compact',
- showSequence: true
- })
-})
-
-test('role page uses helper report defaults as single source of truth', () => {
- const indexSource = readFileSync(new URL('../src/views/system/role/index.vue', import.meta.url), 'utf8')
- const meta = roleHelpers.buildRoleReportMeta()
-
- assert.equal(roleHelpers.ROLE_REPORT_TITLE, '瑙掕壊绠$悊鎶ヨ〃')
- assert.deepEqual(roleHelpers.ROLE_REPORT_STYLE, {
- titleAlign: 'center',
- titleLevel: 'strong'
- })
- assert.equal(meta.reportTitle, roleHelpers.ROLE_REPORT_TITLE)
- assert.deepEqual(meta.reportStyle, {
- ...roleHelpers.ROLE_REPORT_STYLE,
- orientation: 'portrait',
- density: 'compact',
- showSequence: true
- })
- assert.match(
- indexSource,
- /import\s*\{[\s\S]*\bROLE_REPORT_STYLE\b,[\s\S]*\bROLE_REPORT_TITLE\b[\s\S]*\}\s*from '\.\/rolePage\.helpers'/
- )
- assert.match(
- indexSource,
- /<ListExportPrint[\s\S]*:report-title="reportTitle"[\s\S]*:preview-meta="resolvedPreviewMeta"[\s\S]*\/>/
- )
- assert.match(indexSource, /const reportTitle = ROLE_REPORT_TITLE/)
- assert.doesNotMatch(indexSource, /const reportTitle = '瑙掕壊绠$悊鎶ヨ〃'/)
- assert.doesNotMatch(indexSource, /const ROLE_REPORT_STYLE = \{/)
- assert.match(
- indexSource,
- /const resolvedPreviewMeta = computed\(\(\) =>\s*buildRoleReportMeta\(\{\s*previewMeta: previewMeta\.value,\s*count: previewRows\.value\.length,\s*titleAlign: ROLE_REPORT_STYLE\.titleAlign,\s*titleLevel: ROLE_REPORT_STYLE\.titleLevel\s*\}\)\s*\)/
- )
-})
diff --git a/rsf-design/tests/system-role-scope-contract.test.mjs b/rsf-design/tests/system-role-scope-contract.test.mjs
deleted file mode 100644
index c44030d..0000000
--- a/rsf-design/tests/system-role-scope-contract.test.mjs
+++ /dev/null
@@ -1,223 +0,0 @@
-import assert from 'node:assert/strict'
-import fs from 'node:fs'
-import test from 'node:test'
-
-import {
- buildRoleDialogModel,
- buildRolePageQueryParams,
- buildRoleSavePayload,
- buildRoleScopeSubmitPayload,
- buildRoleSearchParams,
- getRolePaginationKey,
- getRoleScopeConfig,
- normalizeRoleScopeTreeData,
- normalizeRoleListRow,
- createRoleSearchState
-} from '../src/views/system/role/rolePage.helpers.js'
-import { resolveBackendMenuTitle } from '../src/utils/backend-menu-title.js'
-
-const rolePermissionDialogSource = fs.readFileSync(
- new URL('../src/views/system/role/modules/role-permission-dialog.vue', import.meta.url),
- 'utf8'
-)
-const roleEditDialogSource = fs.readFileSync(
- new URL('../src/views/system/role/modules/role-edit-dialog.vue', import.meta.url),
- 'utf8'
-)
-const roleIndexSource = fs.readFileSync(
- new URL('../src/views/system/role/index.vue', import.meta.url),
- 'utf8'
-)
-const roleTableColumnsSource = fs.readFileSync(
- new URL('../src/views/system/role/roleTable.columns.js', import.meta.url),
- 'utf8'
-)
-
-test('buildRoleSearchParams keeps real role search fields', () => {
- assert.deepEqual(
- buildRoleSearchParams({
- name: '绠$悊鍛�',
- code: 'R_ADMIN',
- memo: '鏍稿績瑙掕壊',
- status: 1,
- condition: 'admin'
- }),
- {
- name: '绠$悊鍛�',
- code: 'R_ADMIN',
- memo: '鏍稿績瑙掕壊',
- status: 1,
- condition: 'admin'
- }
- )
-})
-
-test('buildRolePageQueryParams merges paging and search fields', () => {
- assert.deepEqual(
- buildRolePageQueryParams({
- current: 3,
- size: 20,
- name: '绠$悊鍛�'
- }),
- {
- current: 3,
- pageSize: 20,
- name: '绠$悊鍛�'
- }
- )
-})
-
-test('role page uses backend pageSize pagination key', () => {
- assert.deepEqual(getRolePaginationKey(), {
- current: 'current',
- size: 'pageSize'
- })
-})
-
-test('buildRoleDialogModel normalizes backend role data into the form model', () => {
- assert.deepEqual(
- buildRoleDialogModel({
- id: 7,
- name: '绠$悊鍛�',
- code: 'R_ADMIN',
- memo: '鏍稿績瑙掕壊',
- status: 0
- }),
- {
- id: 7,
- name: '绠$悊鍛�',
- code: 'R_ADMIN',
- memo: '鏍稿績瑙掕壊',
- status: 0
- }
- )
-})
-
-test('buildRoleSavePayload submits backend role fields', () => {
- assert.deepEqual(
- buildRoleSavePayload({
- id: 7,
- name: '绠$悊鍛�',
- code: 'R_ADMIN',
- memo: '鏍稿績瑙掕壊',
- status: 1
- }),
- {
- id: 7,
- name: '绠$悊鍛�',
- code: 'R_ADMIN',
- memo: '鏍稿績瑙掕壊',
- status: 1
- }
- )
-})
-
-test('normalizeRoleListRow exposes table friendly fields', () => {
- const normalized = normalizeRoleListRow({
- id: 7,
- name: '绠$悊鍛�',
- code: 'R_ADMIN',
- memo: '鏍稿績瑙掕壊',
- status: 1,
- createTime: '2025-03-28 10:00:00',
- updateTime: '2025-03-28 11:00:00'
- })
-
- assert.equal(normalized.name, '绠$悊鍛�')
- assert.equal(normalized.statusBool, true)
- assert.equal(normalized.statusText, '姝e父')
- assert.equal(normalized.statusType, 'success')
- assert.equal(normalized.createTimeText, '2025-03-28 10:00:00')
- assert.equal(normalized.updateTimeText, '2025-03-28 11:00:00')
-})
-
-test('buildRoleScopeSubmitPayload normalizes checked keys for backend scope update', () => {
- assert.deepEqual(
- buildRoleScopeSubmitPayload(7, ['1', 2], ['3']),
- {
- id: 7,
- menuIds: {
- checked: [1, 2],
- halfChecked: [3]
- }
- }
- )
-})
-
-test('getRoleScopeConfig resolves the four backend scope contracts', () => {
- assert.deepEqual(getRoleScopeConfig('menu'), {
- scopeType: 'menu',
- title: '缃戦〉鏉冮檺',
- listUrl: '/role/scope/list',
- treeUrl: '/menu/tree'
- })
- assert.deepEqual(getRoleScopeConfig('warehouse'), {
- scopeType: 'warehouse',
- title: '浠撳簱鏉冮檺',
- listUrl: '/roleWarehouse/scope/list',
- treeUrl: '/menuWarehouse/tree'
- })
- assert.throws(() => getRoleScopeConfig('invalid'), /Unsupported scope type/i)
-})
-
-test('normalizeRoleScopeTreeData folds menu auth buttons into child nodes', () => {
- const tree = normalizeRoleScopeTreeData('menu', [
- {
- id: 1,
- name: 'menu.role',
- type: 0,
- children: [
- {
- id: 2,
- name: 'Query Role',
- type: 1,
- authority: 'system:role:list'
- }
- ]
- }
- ])
-
- assert.equal(tree[0].label, 'menus.role')
- assert.equal(tree[0].children[0].isAuthButton, true)
- assert.equal(tree[0].children[0].authMark, 'system:role:list')
-})
-
-test('resolveBackendMenuTitle translates legacy backend menu keys into readable labels', () => {
- assert.equal(resolveBackendMenuTitle('menu.role'), '瑙掕壊绠$悊')
- assert.equal(resolveBackendMenuTitle('menus.aiParam'), 'AI 鍙傛暟')
- assert.equal(resolveBackendMenuTitle('AI绠$悊涓績'), 'AI绠$悊涓績')
-})
-
-test('role permission dialog only loads the active scope instead of all scopes together', () => {
- assert.match(rolePermissionDialogSource, /ensureScopeLoaded\(activeScopeType\.value, \{ force: true \}\)/)
- assert.doesNotMatch(rolePermissionDialogSource, /loadAllScopeData/)
-})
-
-test('role permission dialog keeps scope tree search and readable labels', () => {
- assert.match(rolePermissionDialogSource, /placeholder="鎼滅储鏉冮檺鏍�"/)
- assert.match(rolePermissionDialogSource, /reloadSelection: false/)
- assert.match(rolePermissionDialogSource, /resolveBackendMenuTitle/)
-})
-
-test('role edit dialog keeps code optional to match the backend contract', () => {
- assert.match(roleEditDialogSource, /name: \[\{ required: true, message: '璇疯緭鍏ヨ鑹插悕绉�'/)
- assert.doesNotMatch(roleEditDialogSource, /code: \[\{ required: true, message: '璇疯緭鍏ヨ鑹茬紪鐮�'/)
-})
-
-test('role page uses semantic auth aliases so backend permissions render actions', () => {
- assert.match(roleIndexSource, /v-auth=\"'add'\"/)
- assert.match(roleIndexSource, /v-auth=\"'delete'\"/)
- assert.match(roleIndexSource, /v-auth=\"'query'\"/)
- assert.match(roleTableColumnsSource, /auth: 'edit'/)
- assert.match(roleTableColumnsSource, /auth: 'delete'/)
-})
-
-test('createRoleSearchState exposes the role search form model', () => {
- assert.deepEqual(createRoleSearchState(), {
- name: '',
- code: '',
- memo: '',
- status: void 0,
- condition: ''
- })
-})
diff --git a/rsf-design/tests/system-user-page-contract.test.mjs b/rsf-design/tests/system-user-page-contract.test.mjs
deleted file mode 100644
index 484333b..0000000
--- a/rsf-design/tests/system-user-page-contract.test.mjs
+++ /dev/null
@@ -1,217 +0,0 @@
-import assert from 'node:assert/strict'
-import test from 'node:test'
-
-import {
- buildUserDialogModel,
- buildUserPageQueryParams,
- buildUserSavePayload,
- buildUserSearchParams,
- getUserPaginationKey,
- getUserStatusMeta,
- mergeUserDetailRecord,
- normalizeDeptTreeOptions,
- normalizeRoleOptions,
- normalizeUserListRow
-} from '../src/views/system/user/userPage.helpers.js'
-
-test('buildUserSearchParams keeps real user page search fields', () => {
- assert.deepEqual(
- buildUserSearchParams({
- username: 'root',
- nickname: '绠$悊鍛�',
- phone: '13800000000',
- email: 'root@example.com',
- status: 1,
- deptId: 0,
- roleIds: [3, 8],
- code: 'A001',
- sex: 1,
- realName: 'Vincent',
- idCard: '330421199511233211',
- condition: 'root'
- }),
- {
- username: 'root',
- nickname: '绠$悊鍛�',
- phone: '13800000000',
- email: 'root@example.com',
- status: 1,
- deptId: 0,
- roleIds: [3, 8],
- code: 'A001',
- sex: 1,
- realName: 'Vincent',
- idCard: '330421199511233211',
- condition: 'root'
- }
- )
-})
-
-test('buildUserPageQueryParams merges paging and search fields', () => {
- assert.deepEqual(
- buildUserPageQueryParams({
- current: 2,
- size: 20,
- username: 'root',
- condition: 'manager'
- }),
- {
- current: 2,
- pageSize: 20,
- username: 'root',
- condition: 'manager'
- }
- )
-})
-
-test('user page uses backend pageSize pagination key', () => {
- assert.deepEqual(getUserPaginationKey(), {
- current: 'current',
- size: 'pageSize'
- })
-})
-
-test('buildUserDialogModel normalizes backend edit data into the form model', () => {
- assert.deepEqual(
- buildUserDialogModel({
- id: 7,
- username: 'root',
- nickname: '绠$悊鍛�',
- deptId: 1,
- roles: [{ id: 3 }, { roleId: 8 }],
- sex: 1,
- code: 'A001',
- phone: '13800000000',
- email: 'root@example.com',
- realName: 'Vincent',
- idCard: '330421199511233211',
- memo: 'memo',
- status: 0
- }),
- {
- id: 7,
- username: 'root',
- nickname: '绠$悊鍛�',
- deptId: 1,
- roleIds: [3, 8],
- userRoleIds: [3, 8],
- password: '',
- confirmPassword: '',
- sex: 1,
- code: 'A001',
- phone: '13800000000',
- email: 'root@example.com',
- realName: 'Vincent',
- idCard: '330421199511233211',
- memo: 'memo',
- status: 0
- }
- )
-})
-
-test('buildUserSavePayload submits roleIds and password for the backend', () => {
- assert.deepEqual(
- buildUserSavePayload({
- id: 7,
- username: 'root',
- nickname: '绠$悊鍛�',
- deptId: 1,
- userRoleIds: [3, 8],
- password: 'secret',
- confirmPassword: 'secret',
- sex: 1,
- code: 'A001',
- phone: '13800000000',
- email: 'root@example.com',
- realName: 'Vincent',
- idCard: '330421199511233211',
- memo: 'memo',
- status: 1
- }),
- {
- id: 7,
- username: 'root',
- nickname: '绠$悊鍛�',
- deptId: 1,
- roleIds: [3, 8],
- password: 'secret',
- sex: 1,
- code: 'A001',
- phone: '13800000000',
- email: 'root@example.com',
- realName: 'Vincent',
- idCard: '330421199511233211',
- memo: 'memo',
- status: 1
- }
- )
-})
-
-test('normalizeUserListRow exposes table friendly fields', () => {
- const normalized = normalizeUserListRow({
- username: 'root',
- nickname: '绠$悊鍛�',
- deptLabel: '鐮斿彂閮�',
- deptId$: '鐮斿彂閮�',
- roleNames: '瓒呯骇绠$悊鍛樸�丱PS',
- roles: [{ name: '瓒呯骇绠$悊鍛�' }, { code: 'OPS' }],
- status: 1,
- createTime$: '2025-03-28 10:00:00',
- updateTime: '2025-03-28 11:00:00'
- })
-
- assert.equal(normalized.username, 'root')
- assert.equal(normalized.deptLabel, '鐮斿彂閮�')
- assert.equal(normalized.roleNames, '瓒呯骇绠$悊鍛樸�丱PS')
- assert.equal(normalized.statusBool, true)
- assert.equal(normalized.statusText, '姝e父')
- assert.equal(normalized.statusType, 'success')
- assert.equal(normalized.createTimeText, '2025-03-28 10:00:00')
- assert.equal(normalized.updateTimeText, '2025-03-28 11:00:00')
-})
-
-test('mergeUserDetailRecord keeps list row roles when detail omits them', () => {
- assert.deepEqual(
- mergeUserDetailRecord(
- {
- id: 7,
- username: 'root',
- nickname: '绠$悊鍛�'
- },
- {
- id: 7,
- deptLabel: '鐮斿彂閮�',
- roleNames: '瓒呯骇绠$悊鍛�',
- roles: [{ id: 3, name: '瓒呯骇绠$悊鍛�' }]
- }
- ),
- {
- id: 7,
- username: 'root',
- nickname: '绠$悊鍛�',
- deptLabel: '鐮斿彂閮�',
- roleNames: '瓒呯骇绠$悊鍛�',
- roles: [{ id: 3, name: '瓒呯骇绠$悊鍛�' }]
- }
- )
-})
-
-test('normalizeDeptTreeOptions and normalizeRoleOptions adapt lookup data', () => {
- assert.deepEqual(
- normalizeDeptTreeOptions([{ id: 1, name: '鎬婚儴', children: [{ id: 2, name: '鐮斿彂閮�' }] }]),
- [{ value: 1, label: '鎬婚儴', children: [{ value: 2, label: '鐮斿彂閮�', children: [] }] }]
- )
-
- assert.deepEqual(
- normalizeRoleOptions([{ id: 3, name: '绠$悊鍛�' }, { roleId: 8, code: 'OPS' }]),
- [
- { value: 3, label: '绠$悊鍛�' },
- { value: 8, label: 'OPS' }
- ]
- )
-})
-
-test('getUserStatusMeta maps enabled and disabled states', () => {
- assert.deepEqual(getUserStatusMeta(1), { type: 'success', text: '姝e父', bool: true })
- assert.deepEqual(getUserStatusMeta(0), { type: 'danger', text: '绂佺敤', bool: false })
-})
diff --git a/rsf-design/tests/user-login-page-contract.test.mjs b/rsf-design/tests/user-login-page-contract.test.mjs
deleted file mode 100644
index 4291b8a..0000000
--- a/rsf-design/tests/user-login-page-contract.test.mjs
+++ /dev/null
@@ -1,30 +0,0 @@
-import assert from 'node:assert/strict'
-import test from 'node:test'
-
-const { createUserLoginApiParams, userLoginPaginationKey } = await import(
- '../src/views/system/user-login/userLoginTable.config.js'
-)
-
-test('user login page uses the backend pageSize pagination contract', () => {
- assert.deepEqual(userLoginPaginationKey, {
- current: 'current',
- size: 'pageSize'
- })
-})
-
-test('user login page builds initial params with pageSize instead of size', () => {
- assert.deepEqual(
- createUserLoginApiParams({
- token: 'abc',
- ip: '127.0.0.1',
- system: 'wms'
- }),
- {
- current: 1,
- pageSize: 20,
- token: 'abc',
- ip: '127.0.0.1',
- system: 'wms'
- }
- )
-})
diff --git a/rsf-design/tests/work-tab-icon-contract.test.mjs b/rsf-design/tests/work-tab-icon-contract.test.mjs
deleted file mode 100644
index 4e1111c..0000000
--- a/rsf-design/tests/work-tab-icon-contract.test.mjs
+++ /dev/null
@@ -1,17 +0,0 @@
-import assert from 'node:assert/strict'
-import fs from 'node:fs'
-import path from 'node:path'
-import test from 'node:test'
-import { fileURLToPath } from 'node:url'
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url))
-const projectRoot = path.resolve(__dirname, '..')
-const workTabSource = fs.readFileSync(
- path.join(projectRoot, 'src/components/core/layouts/art-work-tab/index.vue'),
- 'utf8'
-)
-
-test('work tabs only render the leading icon when a tab actually has an icon', () => {
- assert.match(workTabSource, /<ArtSvgIcon\s+v-if="item\.icon"/)
- assert.doesNotMatch(workTabSource, /<ArtSvgIcon\s+v-show="item\.icon"/)
-})
diff --git a/rsf-design/tests/worktab-icon-normalization-contract.test.mjs b/rsf-design/tests/worktab-icon-normalization-contract.test.mjs
deleted file mode 100644
index 510c4b8..0000000
--- a/rsf-design/tests/worktab-icon-normalization-contract.test.mjs
+++ /dev/null
@@ -1,19 +0,0 @@
-import assert from 'node:assert/strict'
-import fs from 'node:fs'
-import path from 'node:path'
-import test from 'node:test'
-import { fileURLToPath } from 'node:url'
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url))
-const projectRoot = path.resolve(__dirname, '..')
-const worktabSource = fs.readFileSync(
- path.join(projectRoot, 'src/store/modules/worktab.js'),
- 'utf8'
-)
-
-test('worktab store normalizes persisted legacy icon names before rendering tabs', () => {
- assert.match(worktabSource, /import\s+\{\s*normalizeIcon\s*\}\s+from\s+'@\/router\/adapters\/backendMenuAdapter\.js'/)
- assert.match(worktabSource, /icon:\s*normalizeIcon\(/)
- assert.match(worktabSource, /const validTabs = opened\.value\.filter\(\(tab\) => isTabRouteValid\(tab\)\)\.map\(normalizeTabState\)/)
- assert.match(worktabSource, /opened\.value\s*=\s*validTabs/)
-})
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java
index 3846664..b910240 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/BasContainerController.java
@@ -1,18 +1,25 @@
package com.vincent.rsf.server.manager.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
-import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.KeyValVo;
import com.vincent.rsf.server.common.domain.PageParam;
+import com.vincent.rsf.server.common.service.ListExportHandler;
+import com.vincent.rsf.server.common.service.ListExportService;
+import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.manager.entity.BasContainer;
import com.vincent.rsf.server.manager.entity.BasStation;
+import com.vincent.rsf.server.manager.entity.WarehouseAreas;
import com.vincent.rsf.server.manager.service.BasContainerService;
+import com.vincent.rsf.server.manager.service.WarehouseAreasService;
import com.vincent.rsf.server.system.controller.BaseController;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -25,6 +32,12 @@
@Autowired
private BasContainerService basContainerService;
+
+ @Autowired
+ private WarehouseAreasService warehouseAreasService;
+
+ @Autowired
+ private ListExportService listExportService;
@PreAuthorize("hasAuthority('manager:basContainer:list')")
@PostMapping("/basContainer/page")
@@ -123,7 +136,124 @@
@PreAuthorize("hasAuthority('manager:basContainer:list')")
@PostMapping("/basContainer/export")
public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
- ExcelUtil.build(ExcelUtil.create(basContainerService.list(), BasContainer.class), response);
+ Map<Long, String> areaNameMap = buildAreaNameMap();
+ listExportService.export(
+ map,
+ exportMap -> buildParam(exportMap, BaseParam.class),
+ new ListExportHandler<BasContainer, BaseParam>() {
+ @Override
+ public List<BasContainer> listByIds(List<Long> ids) {
+ List<BasContainer> records = basContainerService.listByIds(ids);
+ records.forEach(BasContainer::sortAreas);
+ return records;
+ }
+
+ @Override
+ public List<BasContainer> listByFilter(Map<String, Object> sanitizedMap, BaseParam baseParam) {
+ PageParam<BasContainer, BaseParam> pageParam = new PageParam<>(baseParam, BasContainer.class);
+ QueryWrapper<BasContainer> queryWrapper = pageParam.buildWrapper(true);
+ List<BasContainer> records = basContainerService.list(queryWrapper);
+ records.forEach(BasContainer::sortAreas);
+ return records;
+ }
+
+ @Override
+ public void fillExportFields(List<BasContainer> records) {
+ records.forEach(BasContainer::sortAreas);
+ }
+
+ @Override
+ public Map<String, Object> toExportRow(BasContainer record, List<ExcelUtil.ExportColumn> columns) {
+ return buildExportRow(record, columns, areaNameMap);
+ }
+
+ @Override
+ public String defaultReportTitle() {
+ return "瀹瑰櫒瑙勫垯鎶ヨ〃";
+ }
+ },
+ response
+ );
+ }
+
+ private Map<Long, String> buildAreaNameMap() {
+ Map<Long, String> areaNameMap = new HashMap<>();
+ for (WarehouseAreas area : warehouseAreasService.list()) {
+ if (area != null && area.getId() != null) {
+ areaNameMap.put(area.getId(), area.getName());
+ }
+ }
+ return areaNameMap;
+ }
+
+ private Map<String, Object> buildExportRow(
+ BasContainer record,
+ List<ExcelUtil.ExportColumn> columns,
+ Map<Long, String> areaNameMap
+ ) {
+ BeanWrapper beanWrapper = new BeanWrapperImpl(record);
+ Map<String, Object> row = new LinkedHashMap<>();
+ for (ExcelUtil.ExportColumn column : columns) {
+ Object value = resolveExportValue(record, column.getSource(), beanWrapper, areaNameMap);
+ row.put(column.getSource(), value);
+ }
+ return row;
+ }
+
+ private Object resolveExportValue(
+ BasContainer record,
+ String source,
+ BeanWrapper beanWrapper,
+ Map<Long, String> areaNameMap
+ ) {
+ if ("containerTypeText".equals(source)) {
+ return record.getContainerType$();
+ }
+ if ("areasText".equals(source)) {
+ return buildAreasText(record, areaNameMap);
+ }
+ if ("status".equals(source)) {
+ return record.getStatusBool() == null ? "" : (record.getStatusBool() ? "姝e父" : "鍐荤粨");
+ }
+ if ("createByText".equals(source)) {
+ return record.getCreateBy$();
+ }
+ if ("createTimeText".equals(source)) {
+ return record.getCreateTime$();
+ }
+ if ("updateByText".equals(source)) {
+ return record.getUpdateBy$();
+ }
+ if ("updateTimeText".equals(source)) {
+ return record.getUpdateTime$();
+ }
+ if (beanWrapper.isReadableProperty(source)) {
+ return beanWrapper.getPropertyValue(source);
+ }
+ return null;
+ }
+
+ private String buildAreasText(BasContainer record, Map<Long, String> areaNameMap) {
+ if (record == null || Cools.isEmpty(record.getAreas())) {
+ return "";
+ }
+ List<String> labels = new ArrayList<>();
+ for (Map<String, Object> area : record.getAreas()) {
+ if (area == null) {
+ continue;
+ }
+ Object idValue = area.get("id");
+ if (idValue == null) {
+ continue;
+ }
+ Long areaId = Long.valueOf(String.valueOf(idValue));
+ String label = areaNameMap.get(areaId);
+ if (Cools.isEmpty(label)) {
+ label = String.valueOf(idValue);
+ }
+ labels.add(label);
+ }
+ return String.join("銆�", labels);
}
}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/StockStatisticController.java b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/StockStatisticController.java
index 7e8b86a..67ea781 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/StockStatisticController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/StockStatisticController.java
@@ -41,8 +41,10 @@
BaseParam baseParam = buildParam(map, BaseParam.class);
PageParam<StockStatistic, BaseParam> pageParam = new PageParam<>(baseParam, StockStatistic.class);
QueryWrapper<StockStatistic> wrapper = pageParam.buildWrapper(true);
- wrapper.select("id, loc_code, day_time, task_type, task_status, barcode, maktx, matnr_code, batch, SUM(anfme) AS anfme, unit, fields_index, create_time, update_time");
- wrapper.groupBy("matnr_code, day_time");
+ wrapper.select("MIN(id) AS id, day_time, task_type, task_status, " +
+ "MAX(maktx) AS maktx, matnr_code, MAX(batch) AS batch, " +
+ "SUM(anfme) AS anfme, MAX(unit) AS unit");
+ wrapper.groupBy("day_time, task_type, task_status, matnr_code");
return R.ok().add(stockStatisticService.page(pageParam, wrapper));
}
@@ -52,8 +54,10 @@
BaseParam baseParam = buildParam(map, BaseParam.class);
PageParam<StockStatistic, BaseParam> pageParam = new PageParam<>(baseParam, StockStatistic.class);
QueryWrapper<StockStatistic> wrapper = pageParam.buildWrapper(true);
- wrapper.select("id, loc_code, day_time, task_type, task_status, barcode, maktx, matnr_code, batch, SUM(anfme) AS anfme, unit, fields_index, create_time, update_time");
- wrapper.groupBy("matnr_code, day_time");
+ wrapper.select("MIN(id) AS id, day_time, task_type, task_status, " +
+ "MAX(maktx) AS maktx, matnr_code, MAX(batch) AS batch, " +
+ "SUM(anfme) AS anfme, MAX(unit) AS unit");
+ wrapper.groupBy("day_time, task_type, task_status, matnr_code");
return R.ok().add(stockStatisticService.page(pageParam, wrapper));
}
@@ -63,8 +67,10 @@
BaseParam baseParam = buildParam(map, BaseParam.class);
PageParam<StockStatistic, BaseParam> pageParam = new PageParam<>(baseParam, StockStatistic.class);
QueryWrapper<StockStatistic> wrapper = pageParam.buildWrapper(true);
- wrapper.select("id, loc_code, day_time, task_type, task_status, barcode, maktx, matnr_code, batch, SUM(anfme) anfme, unit, fields_index, create_by, update_by, create_time, update_time");
- wrapper.groupBy("matnr_code, day_time, task_type, task_status");
+ wrapper.select("MIN(id) AS id, loc_code, day_time, task_type, task_status, barcode, " +
+ "MAX(maktx) AS maktx, matnr_code, MAX(batch) AS batch, SUM(anfme) AS anfme, " +
+ "MAX(unit) AS unit, create_by, update_by, create_time, update_time");
+ wrapper.groupBy("loc_code, day_time, task_type, task_status, barcode, matnr_code, create_by, update_by, create_time, update_time");
return R.ok().add(stockStatisticService.page(pageParam, wrapper));
}
@@ -74,8 +80,10 @@
BaseParam baseParam = buildParam(map, BaseParam.class);
PageParam<StockStatistic, BaseParam> pageParam = new PageParam<>(baseParam, StockStatistic.class);
QueryWrapper<StockStatistic> wrapper = pageParam.buildWrapper(true);
- wrapper.select("id, loc_code, day_time, task_type, task_status, barcode, maktx, matnr_code, batch, SUM(anfme) anfme, unit, fields_index, create_by, update_by, create_time, update_time");
- wrapper.groupBy("matnr_code, day_time, task_type, task_status");
+ wrapper.select("MIN(id) AS id, loc_code, day_time, task_type, task_status, barcode, " +
+ "MAX(maktx) AS maktx, matnr_code, MAX(batch) AS batch, SUM(anfme) AS anfme, " +
+ "MAX(unit) AS unit, create_by, update_by, create_time, update_time");
+ wrapper.groupBy("loc_code, day_time, task_type, task_status, barcode, matnr_code, create_by, update_by, create_time, update_time");
return R.ok().add(stockStatisticService.page(pageParam, wrapper));
}
@@ -85,7 +93,7 @@
BaseParam baseParam = buildParam(map, BaseParam.class);
PageParam<StockStatistic, BaseParam> pageParam = new PageParam<>(baseParam, StockStatistic.class);
QueryWrapper<StockStatistic> wrapper = pageParam.buildWrapper(true);
- wrapper.select("id, day_time, COUNT( barcode ) `count`, " +
+ wrapper.select("MIN(id) AS id, day_time, COUNT(barcode) AS `count`, " +
"SUM( anfme ) anfme," +
"COUNT(IF (task_type = 1, 0, NULL)) in_anfme_count, " +
"COUNT(IF ( task_type = 101, 0, NULL)) out_anfme_count, " +
--
Gitblit v1.9.1