zhou zhou
13 小时以前 0d93ec4c10d146ffe287e7f4430ee66ad5832a17
#页面优化
14个文件已添加
32个文件已修改
7628 ■■■■■ 已修改文件
.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/asn-order.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/bas-station.js 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/delivery.js 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/preparation-item.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/preparation.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/en.json 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/zh.json 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-container/modules/bas-container-areas-editor.vue 266 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-station/basStationPage.helpers.js 267 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-station/basStationTable.columns.js 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-station/index.vue 262 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-station/modules/bas-station-init-dialog.vue 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-station/modules/bas-station-tag-cell.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order-log/asnOrderLogPage.helpers.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order-log/asnOrderLogTable.columns.js 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order-log/index.vue 95 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order-log/modules/asn-order-item-log-panel.vue 447 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order-log/modules/asn-order-log-detail-drawer.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order/asnOrderTable.columns.js 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order/index.vue 440 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order/modules/asn-order-create-by-po-dialog.vue 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue 64 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order/modules/asn-order-dialog.vue 370 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order/modules/asn-order-item-dialog.vue 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order/modules/asn-order-material-dialog.vue 269 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery-item/deliveryItemTable.columns.js 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery-item/index.vue 200 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery-item/modules/delivery-item-dialog.vue 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery-item/modules/delivery-item-manage-panel.vue 441 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery/deliveryPage.helpers.js 118 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery/deliveryTable.columns.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery/index.vue 352 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/delivery/modules/delivery-manage-dialog.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation-item/index.vue 371 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation-item/modules/preparation-item-dialog.vue 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation-item/preparationItemPage.helpers.js 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation-item/preparationItemTable.columns.js 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation/index.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation/modules/preparation-generate-dialog.vue 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation/modules/preparation-task-dialog.vue 259 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation/modules/preparation-wave-dialog.vue 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation/preparationPage.helpers.js 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/preparation/preparationTable.columns.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -41,3 +41,4 @@
/log.path_IS_UNDEFINED/*.log
/log.path_IS_UNDEFINED/*/*.log
.worktrees/
.playwright-cli/
rsf-design/src/api/asn-order.js
@@ -34,6 +34,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: params.orderBy || 'create_time desc',
    ...filterParams(params, ['current', 'pageSize', 'size'])
  }
}
@@ -96,6 +97,33 @@
  })
}
export function fetchSaveAsnOrderWithItems(payload = {}) {
  return request.post({
    url: '/asnOrder/items/save',
    params: payload
  })
}
export function fetchUpdateAsnOrderWithItems(payload = {}) {
  return request.post({
    url: '/asnOrder/items/update',
    params: payload
  })
}
export function fetchDeleteAsnOrder(ids) {
  return request.post({
    url: `/asnOrder/remove/${normalizeIds(ids)}`
  })
}
export function fetchInspectAsnOrder(payload = []) {
  return request.post({
    url: '/asnOrder/inspect',
    params: payload
  })
}
export function fetchPurchaseFilterPage(params = {}) {
  return request.post({
    url: '/purchase/filters/page',
@@ -117,6 +145,12 @@
  })
}
export function fetchEnabledAsnOrderFields() {
  return request.get({
    url: '/fields/enable/list'
  })
}
export async function fetchExportAsnOrderReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/asnOrder/export`, {
    method: 'POST',
@@ -127,3 +161,26 @@
    body: JSON.stringify(payload)
  })
}
export async function fetchDownloadAsnOrderTemplate(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/asnOrderItem/template/download`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})
    },
    body: JSON.stringify(payload)
  })
}
export function fetchImportAsnOrder(file) {
  const formData = new FormData()
  formData.append('file', file)
  return request.post({
    url: '/asnOrderItem/import',
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
}
rsf-design/src/api/bas-station.js
@@ -34,7 +34,8 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    ...filterParams(params, ['current', 'pageSize', 'size'])
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...filterParams(params, ['current', 'pageSize', 'size', 'orderBy'])
  }
}
@@ -48,6 +49,14 @@
        ? Number(params.type)
        : void 0,
    useStatus: normalizeText(params.useStatus),
    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,
    area:
      params.area !== undefined && params.area !== null && params.area !== ''
        ? Number(params.area)
@@ -56,13 +65,23 @@
      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:
      params.containerType !== undefined &&
      params.containerType !== null &&
      params.containerType !== ''
        ? Number(params.containerType)
        : void 0,
    barcode: normalizeText(params.barcode),
    autoTransfer:
      params.autoTransfer !== undefined && params.autoTransfer !== null && params.autoTransfer !== ''
      params.autoTransfer !== undefined &&
      params.autoTransfer !== null &&
      params.autoTransfer !== ''
        ? Number(params.autoTransfer)
        : void 0,
    status:
@@ -75,7 +94,9 @@
  }
  return Object.fromEntries(
    Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
    Object.entries(searchParams).filter(
      ([, value]) => value !== '' && value !== void 0 && value !== null
    )
  )
}
@@ -93,7 +114,9 @@
      ? { area: Number(formData.area) }
      : {}),
    useStatus: normalizeText(formData.useStatus) || '',
    ...(formData.isCrossZone !== undefined && formData.isCrossZone !== null && formData.isCrossZone !== ''
    ...(formData.isCrossZone !== undefined &&
    formData.isCrossZone !== null &&
    formData.isCrossZone !== ''
      ? { isCrossZone: Number(formData.isCrossZone) }
      : {}),
    ...(Array.isArray(formData.areaIds) && formData.areaIds.length
@@ -104,10 +127,16 @@
      : {}),
    wcsData: normalizeText(formData.wcsData) || '',
    ...(Array.isArray(formData.containerTypes) && formData.containerTypes.length
      ? { containerTypes: formData.containerTypes.map((id) => Number(id)).filter((id) => !Number.isNaN(id)) }
      ? {
          containerTypes: formData.containerTypes
            .map((id) => Number(id))
            .filter((id) => !Number.isNaN(id))
        }
      : {}),
    barcode: normalizeText(formData.barcode) || '',
    ...(formData.autoTransfer !== undefined && formData.autoTransfer !== null && formData.autoTransfer !== ''
    ...(formData.autoTransfer !== undefined &&
    formData.autoTransfer !== null &&
    formData.autoTransfer !== ''
      ? { autoTransfer: Number(formData.autoTransfer) }
      : {}),
    ...(formData.inAble !== undefined && formData.inAble !== null && formData.inAble !== ''
rsf-design/src/api/delivery.js
@@ -32,9 +32,27 @@
export function buildDeliverySearchParams(params = {}) {
  const result = {}
  ;['condition', 'code', 'platId', 'type', 'wkType', 'source', 'timeStart', 'timeEnd', 'memo'].forEach((key) => {
  ;[
    'condition',
    'code',
    'platId',
    'type',
    'wkType',
    'source',
    'platCode',
    'timeStart',
    'timeEnd',
    'startTime',
    'endTime',
    'memo'
  ].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) result[key] = value
  })
  ;['anfme', 'qty', 'workQty'].forEach((key) => {
    if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
      result[key] = Number(params[key])
    }
  })
  if (params.status !== '' && params.status !== undefined && params.status !== null) {
    result.status = Number(params.status)
@@ -49,6 +67,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildDeliverySearchParams(params)
  }
}
@@ -74,7 +93,8 @@
    'splrBatch',
    'timeStart',
    'timeEnd',
    'memo'
    'memo',
    'fieldsIndex'
  ].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) result[key] = value
@@ -92,6 +112,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildDeliveryItemSearchParams(params)
  }
}
@@ -134,6 +155,12 @@
  })
}
export function fetchDeleteDeliveryMany(ids) {
  return request.post({
    url: `/delivery/remove/${normalizeIds(ids)}`
  })
}
export function fetchSaveDelivery(payload = {}) {
  return request.post({
    url: '/delivery/save',
@@ -145,6 +172,18 @@
  return request.post({
    url: '/delivery/update',
    params: payload
  })
}
export function fetchImportDelivery(file) {
  const formData = new FormData()
  formData.append('file', file)
  return request.post({
    url: '/delivery/import',
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
}
@@ -173,6 +212,12 @@
  })
}
export function fetchDeleteDeliveryItemMany(ids) {
  return request.post({
    url: `/deliveryItem/remove/${normalizeIds(ids)}`
  })
}
export function fetchSaveDeliveryItem(payload = {}) {
  return request.post({
    url: '/deliveryItem/save',
@@ -198,3 +243,14 @@
    body: JSON.stringify(buildDeliveryExportParams(payload))
  })
}
export async function fetchDownloadDeliveryTemplate(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/delivery/template/download`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})
    },
    body: JSON.stringify(payload)
  })
}
rsf-design/src/api/preparation-item.js
@@ -66,6 +66,26 @@
  })
}
export function fetchSavePreparationItem(payload = {}) {
  return request.post({
    url: '/outStockItem/save',
    params: payload
  })
}
export function fetchUpdatePreparationItem(payload = {}) {
  return request.post({
    url: '/outStockItem/update',
    params: payload
  })
}
export function fetchDeletePreparationItem(ids) {
  return request.post({
    url: `/outStockItem/remove/${normalizeIds(ids)}`
  })
}
export async function fetchExportPreparationItemReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/outStockItem/export`, {
    method: 'POST',
rsf-design/src/api/preparation.js
@@ -51,6 +51,13 @@
  return request.post({ url: '/preparation/page', params: buildPreparationPageParams(params) })
}
export function fetchPreparationDialogPage(params = {}) {
  return request.post({
    url: '/deliveryItem/filters/page',
    params: buildPreparationPageParams(params)
  })
}
export function fetchPreparationItemPage(params = {}) {
  return request.post({
    url: '/outStockItem/page',
@@ -78,6 +85,26 @@
  return request.get({ url: `/preparation/cancel/${id}` })
}
export function fetchGeneratePreparationOrders(payload = {}) {
  return request.post({ url: '/preparation/generate/orders', params: payload })
}
export function fetchGeneratePreparationWave(payload = {}) {
  return request.post({ url: '/preparation/generate/wave', params: payload })
}
export function fetchPreparationTaskPreview(payload = {}) {
  return request.post({ url: '/preparation/order/getOutTaskItems', params: payload })
}
export function fetchGeneratePreparationTasks(payload = {}) {
  return request.post({ url: '/preparation/generate/tasks', params: payload })
}
export function fetchPreparationTaskSites() {
  return request.get({ url: '/preparation/tasks/sites' })
}
export async function fetchExportPreparationReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/preparation/export`, {
    method: 'POST',
rsf-design/src/locales/langs/en.json
@@ -1192,6 +1192,10 @@
        "reportTitle": "ASN Report",
        "entity": "ASN",
        "buttons": {
          "create": "New ASN",
          "import": "Import",
          "downloadTemplate": "Download Template",
          "inspection": "Batch Inspection",
          "createByPo": "Create by PO"
        },
        "search": {
@@ -1201,8 +1205,14 @@
          "codePlaceholder": "Enter ASN No.",
          "poCode": "PO No.",
          "poCodePlaceholder": "Enter PO No.",
          "poId": "PO ID",
          "type": "Order Type",
          "wkType": "Business Type",
          "wkTypePlaceholder": "Enter business type",
          "anfme": "Delivery Qty",
          "qty": "Received Qty",
          "logisNo": "Logistics No.",
          "arrTime": "Estimated Arrival",
          "memo": "Remark",
          "exceStatus": "Document Status",
          "supplierName": "Supplier",
          "supplierPlaceholder": "Enter supplier",
@@ -1213,7 +1223,14 @@
          "condition": "Enter ASN No./PO No./Supplier",
          "code": "Enter ASN No.",
          "poCode": "Enter PO No.",
          "wkType": "Enter business type",
          "poId": "Enter PO ID",
          "type": "Select order type",
          "wkType": "Select business type",
          "anfme": "Enter delivery qty",
          "qty": "Enter received qty",
          "logisNo": "Enter logistics No.",
          "arrTime": "Select estimated arrival time",
          "memo": "Enter remark",
          "supplierName": "Enter supplier",
          "purchaseUserName": "Enter purchaser"
        },
@@ -1229,6 +1246,8 @@
        "actions": {
          "view": "View Detail",
          "items": "Receiving Items",
          "edit": "Edit",
          "delete": "Delete",
          "print": "Print",
          "complete": "Complete"
        },
@@ -1256,6 +1275,77 @@
          "actionFailed": "ASN action failed",
          "detailTimeout": "ASN detail items timed out and waiting has stopped",
          "itemsTimeout": "ASN detail items timed out and waiting has stopped"
        },
        "dialog": {
          "createTitle": "Create ASN",
          "editTitle": "Edit ASN",
          "addMaterial": "Add Material",
          "deleteSelected": "Delete Selected",
          "itemCount": "{count} item(s)",
          "materialDuplicate": "The selected materials already exist in current items",
          "placeholder": {
            "type": "Select order type",
            "wkType": "Select business type",
            "poCode": "Enter PO No.",
            "logisNo": "Enter logistics No.",
            "arrTime": "Select estimated arrival time",
            "memo": "Enter remark"
          },
          "validation": {
            "type": "Please select order type",
            "wkType": "Please select business type",
            "items": "Please add at least one material item",
            "anfme": "Expected quantity must be greater than 0"
          }
        },
        "materialDialog": {
          "title": "Select Material",
          "selected": "Selected",
          "unselected": "Available",
          "search": {
            "name": "Material Name",
            "code": "Material Code",
            "groupId": "Material Group"
          },
          "placeholder": {
            "name": "Enter material name",
            "code": "Enter material code",
            "groupId": "Select material group"
          },
          "table": {
            "code": "Material Code",
            "name": "Material Name",
            "group": "Material Group",
            "spec": "Specification",
            "model": "Model",
            "status": "Selection Status"
          },
          "messages": {
            "groupTimeout": "Material group loading timed out and waiting has stopped"
          }
        },
        "messages": {
          "createSuccess": "ASN created successfully",
          "createFailed": "Failed to create ASN",
          "updateSuccess": "ASN updated successfully",
          "updateFailed": "Failed to update ASN",
          "deleteTitle": "Delete Confirmation",
          "deleteConfirm": "Are you sure you want to delete ASN {code}?",
          "deleteSuccess": "ASN deleted successfully",
          "detailTimeout": "ASN detail loading timed out and waiting has stopped",
          "detailFailed": "Failed to load ASN detail",
          "inspectionTitle": "Inspection Confirmation",
          "inspectionConfirm": "Are you sure you want to inspect the selected {count} ASN(s)?",
          "inspectionSuccess": "ASN inspection submitted successfully",
          "inspectionFailed": "Failed to submit ASN inspection",
          "inspectionSelectRequired": "Please select the ASN records to inspect first",
          "importSuccess": "ASN import succeeded",
          "importFailed": "ASN import failed",
          "templateDownloadSuccess": "Template downloaded successfully",
          "templateDownloadFailed": "Failed to download template",
          "typeOptionsTimeout": "Order type options timed out and waiting has stopped",
          "wkTypeOptionsTimeout": "Business type options timed out and waiting has stopped",
          "fieldOptionsTimeout": "Extended field loading timed out and waiting has stopped"
        },
        "createByPoDialog": {
          "title": "Create by PO",
@@ -1287,6 +1377,9 @@
          }
        },
        "table": {
          "purchaseOrgName": "Purchasing Org",
          "businessTime": "Purchase Date",
          "supplierId": "Supplier Code",
          "poItemId": "PO Line No.",
          "expectedQty": "Expected Qty",
          "receivedQty": "Received Qty",
@@ -1556,6 +1649,8 @@
        "search": {
          "condition": "Keyword",
          "conditionPlaceholder": "Enter No./ERP master order/platform order",
          "timeStart": "Created From",
          "timeEnd": "Created To",
          "code": "No.",
          "codePlaceholder": "Enter No.",
          "platId": "ERP Master Order ID",
@@ -1566,6 +1661,12 @@
          "wkTypePlaceholder": "Enter business type",
          "source": "Order Source",
          "sourcePlaceholder": "Enter order source",
          "anfme": "Outbound Qty",
          "qty": "Outbound Done Qty",
          "workQty": "Working Qty",
          "platCode": "Platform Order No.",
          "startTime": "Planned Outbound Time",
          "endTime": "Planned Outbound End Time",
          "exceStatus": "Execution Status",
          "exceStatusPlaceholder": "Enter execution status",
          "memo": "Remark",
@@ -1573,13 +1674,26 @@
        },
        "placeholder": {
          "condition": "Enter No./ERP master order/platform order",
          "timeStart": "Select created from",
          "timeEnd": "Select created to",
          "code": "Enter No.",
          "platId": "Enter ERP master order ID",
          "type": "Enter order type",
          "wkType": "Enter business type",
          "source": "Enter order source",
          "anfme": "Enter outbound qty",
          "qty": "Enter outbound done qty",
          "workQty": "Enter working qty",
          "platCode": "Enter platform order No.",
          "startTime": "Select planned outbound time",
          "endTime": "Select planned outbound end time",
          "status": "Select status",
          "exceStatus": "Enter execution status",
          "memo": "Enter remark"
        },
        "buttons": {
          "import": "Import",
          "downloadTemplate": "Download Template"
        },
        "status": {
          "normal": "Normal",
@@ -1591,8 +1705,13 @@
        },
        "actions": {
          "view": "View Detail",
          "edit": "Edit",
          "items": "Items",
          "delete": "Delete"
        },
        "manage": {
          "title": "Edit DO",
          "baseInfo": "Order Information"
        },
        "detail": {
          "title": "Handover Order Detail",
@@ -1640,7 +1759,11 @@
        "messages": {
          "itemsTimeout": "DO items timed out and waiting has stopped",
          "detailTimeout": "DO detail timed out and waiting has stopped",
          "detailLoadFailed": "Failed to load DO detail"
          "detailLoadFailed": "Failed to load DO detail",
          "importSuccess": "DO imported successfully",
          "importFailed": "Failed to import DO",
          "templateDownloadSuccess": "Template downloaded successfully",
          "templateDownloadFailed": "Failed to download template"
        }
      },
      "deliveryItem": {
@@ -1653,11 +1776,58 @@
          "deliveryCodePlaceholder": "Enter DO No.",
          "platItemId": "Platform Line No.",
          "platItemIdPlaceholder": "Enter platform line No.",
          "fieldsIndexPlaceholder": "Enter field index",
          "matnrCodePlaceholder": "Enter material code",
          "maktxPlaceholder": "Enter material name",
          "supplierName": "Supplier Name",
          "supplierNamePlaceholder": "Enter supplier name",
          "supplierBatchPlaceholder": "Enter supplier batch"
          "supplierCodePlaceholder": "Enter supplier code",
          "supplierBatchPlaceholder": "Enter supplier batch",
          "statusPlaceholder": "Select status",
          "timeStart": "Created From",
          "timeStartPlaceholder": "Select created from",
          "timeEnd": "Created To",
          "timeEndPlaceholder": "Select created to",
          "memoPlaceholder": "Enter remark"
        },
        "actions": {
          "add": "Add Item"
        },
        "dialog": {
          "titleAdd": "Add DO Item",
          "titleEdit": "Edit DO Item",
          "deliveryId": "DO ID",
          "platItemId": "Platform Line No.",
          "matnrCode": "Material Code",
          "maktx": "Material Name",
          "fieldsIndex": "Field Index",
          "unit": "Unit",
          "anfme": "Qty",
          "qty": "Outbound Qty",
          "printQty": "Print Qty",
          "splrName": "Supplier Name",
          "splrCode": "Supplier Code",
          "splrBatch": "Supplier Batch",
          "status": "Status",
          "memo": "Remark",
          "placeholder": {
            "platItemId": "Enter platform line No.",
            "matnrCode": "Enter material code",
            "maktx": "Enter material name",
            "fieldsIndex": "Enter field index",
            "unit": "Enter unit",
            "anfme": "Enter qty",
            "qty": "Enter outbound qty",
            "printQty": "Enter print qty",
            "splrName": "Enter supplier name",
            "splrCode": "Enter supplier code",
            "splrBatch": "Enter supplier batch",
            "status": "Select status",
            "memo": "Enter remark"
          },
          "validation": {
            "anfme": "Enter qty"
          }
        },
        "table": {
          "deliveryId": "DO ID",
@@ -1690,7 +1860,13 @@
        },
        "messages": {
          "detailTimeout": "DO item detail timed out and waiting has stopped",
          "detailFailed": "Failed to load DO item detail"
          "detailFailed": "Failed to load DO item detail",
          "createSuccess": "DO item created successfully",
          "createFailed": "Failed to create DO item",
          "updateSuccess": "DO item updated successfully",
          "updateFailed": "Failed to update DO item",
          "deleteConfirm": "Are you sure you want to delete DO item {code}?",
          "actionFailed": "DO item action failed"
        }
      },
      "transfer": {
rsf-design/src/locales/langs/zh.json
@@ -1194,6 +1194,10 @@
        "reportTitle": "入库通知单报表",
        "entity": "入库通知单",
        "buttons": {
          "create": "新建入库通知单",
          "import": "导入",
          "downloadTemplate": "下载模板",
          "inspection": "批量报检",
          "createByPo": "按PO建单"
        },
        "search": {
@@ -1203,8 +1207,14 @@
          "codePlaceholder": "请输入 ASN 单号",
          "poCode": "PO单号",
          "poCodePlaceholder": "请输入 PO 单号",
          "poId": "PO单ID",
          "type": "单据类型",
          "wkType": "业务类型",
          "wkTypePlaceholder": "请输入业务类型",
          "anfme": "送货数量",
          "qty": "已收数量",
          "logisNo": "物流单号",
          "arrTime": "预计到达时间",
          "memo": "备注",
          "exceStatus": "单据状态",
          "supplierName": "供应商",
          "supplierPlaceholder": "请输入供应商",
@@ -1215,7 +1225,14 @@
          "condition": "请输入 ASN 单号/PO 单号/供应商",
          "code": "请输入 ASN 单号",
          "poCode": "请输入 PO 单号",
          "wkType": "请输入业务类型",
          "poId": "请输入 PO 单号 ID",
          "type": "请选择单据类型",
          "wkType": "请选择业务类型",
          "anfme": "请输入送货数量",
          "qty": "请输入已收数量",
          "logisNo": "请输入物流单号",
          "arrTime": "请选择预计到达时间",
          "memo": "请输入备注",
          "supplierName": "请输入供应商",
          "purchaseUserName": "请输入采购员"
        },
@@ -1231,6 +1248,8 @@
        "actions": {
          "view": "查看详情",
          "items": "收货明细",
          "edit": "编辑",
          "delete": "删除",
          "print": "打印",
          "complete": "完成"
        },
@@ -1258,6 +1277,77 @@
          "actionFailed": "入库通知单操作失败",
          "detailTimeout": "入库通知单明细加载超时,已停止等待",
          "itemsTimeout": "入库通知单明细加载超时,已停止等待"
        },
        "dialog": {
          "createTitle": "新增入库通知单",
          "editTitle": "编辑入库通知单",
          "addMaterial": "新增物料",
          "deleteSelected": "删除选中",
          "itemCount": "当前共 {count} 条明细",
          "materialDuplicate": "所选物料已存在于当前明细中",
          "placeholder": {
            "type": "请选择单据类型",
            "wkType": "请选择业务类型",
            "poCode": "请输入 PO 单号",
            "logisNo": "请输入物流单号",
            "arrTime": "请选择预计到达时间",
            "memo": "请输入备注"
          },
          "validation": {
            "type": "请选择单据类型",
            "wkType": "请选择业务类型",
            "items": "请至少添加一条物料明细",
            "anfme": "物料明细的应收数量必须大于 0"
          }
        },
        "materialDialog": {
          "title": "选择物料",
          "selected": "已选择",
          "unselected": "可选择",
          "search": {
            "name": "物料名称",
            "code": "物料编码",
            "groupId": "物料分组"
          },
          "placeholder": {
            "name": "请输入物料名称",
            "code": "请输入物料编码",
            "groupId": "请选择物料分组"
          },
          "table": {
            "code": "物料编码",
            "name": "物料名称",
            "group": "物料分组",
            "spec": "规格",
            "model": "型号",
            "status": "选择状态"
          },
          "messages": {
            "groupTimeout": "物料分组加载超时,已停止等待"
          }
        },
        "messages": {
          "createSuccess": "入库通知单新增成功",
          "createFailed": "入库通知单新增失败",
          "updateSuccess": "入库通知单更新成功",
          "updateFailed": "入库通知单更新失败",
          "deleteTitle": "删除确认",
          "deleteConfirm": "确定删除入库通知单 {code} 吗?",
          "deleteSuccess": "入库通知单删除成功",
          "detailTimeout": "入库通知单详情加载超时,已停止等待",
          "detailFailed": "获取入库通知单详情失败",
          "inspectionTitle": "报检确认",
          "inspectionConfirm": "确定对选中的 {count} 张入库通知单执行报检吗?",
          "inspectionSuccess": "入库通知单报检成功",
          "inspectionFailed": "入库通知单报检失败",
          "inspectionSelectRequired": "请先选择需要报检的入库通知单",
          "importSuccess": "入库通知单导入成功",
          "importFailed": "入库通知单导入失败",
          "templateDownloadSuccess": "模板下载成功",
          "templateDownloadFailed": "模板下载失败",
          "typeOptionsTimeout": "单据类型选项加载超时,已停止等待",
          "wkTypeOptionsTimeout": "业务类型选项加载超时,已停止等待",
          "fieldOptionsTimeout": "扩展字段加载超时,已停止等待"
        },
        "createByPoDialog": {
          "title": "按PO建单",
@@ -1289,6 +1379,9 @@
          }
        },
        "table": {
          "purchaseOrgName": "采购组织",
          "businessTime": "采购日期",
          "supplierId": "供应商编码",
          "poItemId": "PO行号",
          "expectedQty": "应收数量",
          "receivedQty": "已收数量",
@@ -1564,6 +1657,8 @@
        "search": {
          "condition": "关键字",
          "conditionPlaceholder": "请输入单号/ERP主单标识/平台单号",
          "timeStart": "创建开始时间",
          "timeEnd": "创建结束时间",
          "code": "单号",
          "codePlaceholder": "请输入单号",
          "platId": "ERP主单标识",
@@ -1574,6 +1669,12 @@
          "wkTypePlaceholder": "请输入业务类型",
          "source": "单据来源",
          "sourcePlaceholder": "请输入单据来源",
          "anfme": "出库数量",
          "qty": "已出库数量",
          "workQty": "执行中数量",
          "platCode": "平台单号",
          "startTime": "计划出库时间",
          "endTime": "计划出库结束时间",
          "exceStatus": "执行状态",
          "exceStatusPlaceholder": "请输入执行状态",
          "memo": "备注",
@@ -1581,13 +1682,26 @@
        },
        "placeholder": {
          "condition": "请输入单号/ERP主单标识/平台单号",
          "timeStart": "请选择创建开始时间",
          "timeEnd": "请选择创建结束时间",
          "code": "请输入单号",
          "platId": "请输入ERP主单标识",
          "type": "请输入单据类型",
          "wkType": "请输入业务类型",
          "source": "请输入单据来源",
          "anfme": "请输入出库数量",
          "qty": "请输入已出库数量",
          "workQty": "请输入执行中数量",
          "platCode": "请输入平台单号",
          "startTime": "请选择计划出库时间",
          "endTime": "请选择计划出库结束时间",
          "status": "请选择状态",
          "exceStatus": "请输入执行状态",
          "memo": "请输入备注"
        },
        "buttons": {
          "import": "导入",
          "downloadTemplate": "下载模板"
        },
        "status": {
          "normal": "正常",
@@ -1599,8 +1713,13 @@
        },
        "actions": {
          "view": "查看详情",
          "edit": "编辑",
          "items": "明细",
          "delete": "删除"
        },
        "manage": {
          "title": "编辑DO单",
          "baseInfo": "主单信息"
        },
        "detail": {
          "title": "交接单详情",
@@ -1648,7 +1767,11 @@
        "messages": {
          "itemsTimeout": "DO单明细加载超时,已停止等待",
          "detailTimeout": "DO单详情加载超时,已停止等待",
          "detailLoadFailed": "DO单详情加载失败"
          "detailLoadFailed": "DO单详情加载失败",
          "importSuccess": "DO单导入成功",
          "importFailed": "DO单导入失败",
          "templateDownloadSuccess": "模板下载成功",
          "templateDownloadFailed": "模板下载失败"
        }
      },
      "deliveryItem": {
@@ -1661,11 +1784,58 @@
          "deliveryCodePlaceholder": "请输入DO单号",
          "platItemId": "平台行号",
          "platItemIdPlaceholder": "请输入平台行号",
          "fieldsIndexPlaceholder": "请输入字段索引",
          "matnrCodePlaceholder": "请输入物料编码",
          "maktxPlaceholder": "请输入物料名称",
          "supplierName": "供应商名称",
          "supplierNamePlaceholder": "请输入供应商名称",
          "supplierBatchPlaceholder": "请输入供应商批次"
          "supplierCodePlaceholder": "请输入供应商编码",
          "supplierBatchPlaceholder": "请输入供应商批次",
          "statusPlaceholder": "请选择状态",
          "timeStart": "创建开始时间",
          "timeStartPlaceholder": "请选择创建开始时间",
          "timeEnd": "创建结束时间",
          "timeEndPlaceholder": "请选择创建结束时间",
          "memoPlaceholder": "请输入备注"
        },
        "actions": {
          "add": "新增明细"
        },
        "dialog": {
          "titleAdd": "新增DO单明细",
          "titleEdit": "编辑DO单明细",
          "deliveryId": "DO单ID",
          "platItemId": "平台行号",
          "matnrCode": "物料编码",
          "maktx": "物料名称",
          "fieldsIndex": "字段索引",
          "unit": "单位",
          "anfme": "数量",
          "qty": "已出数量",
          "printQty": "打印数量",
          "splrName": "供应商名称",
          "splrCode": "供应商编码",
          "splrBatch": "供应商批次",
          "status": "状态",
          "memo": "备注",
          "placeholder": {
            "platItemId": "请输入平台行号",
            "matnrCode": "请输入物料编码",
            "maktx": "请输入物料名称",
            "fieldsIndex": "请输入字段索引",
            "unit": "请输入单位",
            "anfme": "请输入数量",
            "qty": "请输入已出数量",
            "printQty": "请输入打印数量",
            "splrName": "请输入供应商名称",
            "splrCode": "请输入供应商编码",
            "splrBatch": "请输入供应商批次",
            "status": "请选择状态",
            "memo": "请输入备注"
          },
          "validation": {
            "anfme": "请输入数量"
          }
        },
        "table": {
          "deliveryId": "DO单ID",
@@ -1698,7 +1868,13 @@
        },
        "messages": {
          "detailTimeout": "DO单明细详情加载超时,已停止等待",
          "detailFailed": "获取DO单明细详情失败"
          "detailFailed": "获取DO单明细详情失败",
          "createSuccess": "DO单明细新增成功",
          "createFailed": "DO单明细新增失败",
          "updateSuccess": "DO单明细修改成功",
          "updateFailed": "DO单明细修改失败",
          "deleteConfirm": "确定删除DO单明细 {code} 吗?",
          "actionFailed": "DO单明细操作失败"
        }
      },
      "transfer": {
rsf-design/src/views/basic-info/bas-container/modules/bas-container-areas-editor.vue
@@ -1,9 +1,9 @@
<template>
  <div class="space-y-3">
    <div class="flex flex-wrap items-center gap-2">
  <div class="areas-editor">
    <div class="areas-editor__toolbar">
      <ElSelect
        v-model="selectedAreaId"
        class="min-w-0 flex-1"
        class="areas-editor__select"
        clearable
        filterable
        placeholder="请选择可入库区"
@@ -15,44 +15,73 @@
          :value="option.value"
        />
      </ElSelect>
      <ElButton :disabled="selectedAreaId === '' || selectedAreaId === void 0" @click="handleAddArea">
      <ElButton
        :disabled="selectedAreaId === '' || selectedAreaId === void 0"
        @click="handleAddArea"
      >
        添加
      </ElButton>
      <span class="areas-editor__toolbar-tip">
        已选 {{ selectedAreas.length }} 个库区,可调整顺序
      </span>
    </div>
    <ElEmpty v-if="!selectedAreas.length" description="暂无可入库区" />
    <div class="areas-editor__panel">
      <template v-if="selectedAreas.length">
        <div class="areas-editor__header">
          <span>序号</span>
          <span>库区</span>
          <span>排序</span>
          <span>操作</span>
        </div>
    <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) }}
        <ElScrollbar max-height="240px">
          <div class="areas-editor__list">
            <div v-for="(item, index) in selectedAreas" :key="item.id" class="areas-editor__row">
              <div class="areas-editor__index">
                {{ index + 1 }}
              </div>
              <div class="areas-editor__name">
                <div class="areas-editor__name-text">
                  {{ resolveAreaLabel(item) }}
                </div>
                <div class="areas-editor__meta"> ID: {{ item.id }} </div>
              </div>
              <div class="areas-editor__sort">
                <ElInputNumber
                  :model-value="item.sort"
                  :min="1"
                  :controls-position="'right'"
                  class="areas-editor__sort-input"
                  @update:model-value="handleSortChange(item.id, $event)"
                />
              </div>
              <div class="areas-editor__actions">
                <ElButton text size="small" :disabled="index === 0" @click="handleMoveUp(index)">
                  上移
                </ElButton>
                <ElButton
                  text
                  size="small"
                  :disabled="index === selectedAreas.length - 1"
                  @click="handleMoveDown(index)"
                >
                  下移
                </ElButton>
                <ElButton text size="small" type="danger" @click="handleRemove(item.id)">
                  删除
                </ElButton>
              </div>
            </div>
          </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>
        </ElScrollbar>
      </template>
      <div v-else class="areas-editor__empty">
        <div class="areas-editor__empty-title">暂无可入库区</div>
        <div class="areas-editor__empty-tip"
          >从上方选择库区后点击“添加”,再按排序值或上下移动调整顺序</div
        >
      </div>
    </div>
  </div>
@@ -70,13 +99,14 @@
  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 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)))
@@ -95,7 +125,9 @@
    }
    return {
      value: Number(value),
      label: String(option.label || option.name || option.areaName || option.code || option.areaCode || value)
      label: String(
        option.label || option.name || option.areaName || option.code || option.areaCode || value
      )
    }
  }
@@ -175,3 +207,151 @@
    syncModel(selectedAreas.value.filter((item) => Number(item.id) !== Number(id)))
  }
</script>
<style scoped>
  .areas-editor {
    display: flex;
    flex-direction: column;
    gap: 12px;
    width: 100%;
  }
  .areas-editor__toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
  }
  .areas-editor__select {
    width: 320px;
    max-width: 100%;
  }
  .areas-editor__toolbar-tip {
    color: var(--art-text-secondary);
    font-size: 12px;
    line-height: 1.5;
  }
  .areas-editor__panel {
    border: 1px solid var(--art-border-color);
    border-radius: 12px;
    background: var(--art-bg-color);
    overflow: hidden;
  }
  .areas-editor__header {
    display: grid;
    grid-template-columns: 56px minmax(0, 1fr) 112px 190px;
    gap: 12px;
    align-items: center;
    padding: 10px 16px;
    background: var(--el-fill-color-light);
    color: var(--art-text-secondary);
    font-size: 12px;
    line-height: 1;
  }
  .areas-editor__list {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 12px 16px;
  }
  .areas-editor__row {
    display: grid;
    grid-template-columns: 56px minmax(0, 1fr) 112px 190px;
    gap: 12px;
    align-items: center;
    border: 1px solid var(--art-border-color);
    border-radius: 10px;
    padding: 10px 12px;
    background: var(--el-bg-color-page);
  }
  .areas-editor__index {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--art-text-secondary);
    font-size: 13px;
  }
  .areas-editor__name {
    min-width: 0;
  }
  .areas-editor__name-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--art-text-primary);
    font-weight: 500;
    line-height: 1.4;
  }
  .areas-editor__meta {
    margin-top: 2px;
    color: var(--art-text-secondary);
    font-size: 12px;
    line-height: 1.4;
  }
  .areas-editor__sort {
    display: flex;
    justify-content: flex-start;
  }
  :deep(.areas-editor__sort-input) {
    width: 100px;
  }
  .areas-editor__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
  }
  .areas-editor__empty {
    display: flex;
    min-height: 136px;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 20px 16px;
    text-align: center;
  }
  .areas-editor__empty-title {
    color: var(--art-text-primary);
    font-size: 14px;
    font-weight: 500;
    line-height: 1.4;
  }
  .areas-editor__empty-tip {
    max-width: 420px;
    color: var(--art-text-secondary);
    font-size: 12px;
    line-height: 1.6;
  }
  @media (max-width: 900px) {
    .areas-editor__header {
      display: none;
    }
    .areas-editor__row {
      grid-template-columns: 48px minmax(0, 1fr);
    }
    .areas-editor__sort,
    .areas-editor__actions {
      grid-column: 2;
    }
  }
</style>
rsf-design/src/views/basic-info/bas-station/basStationPage.helpers.js
@@ -91,9 +91,14 @@
    stationId: '',
    type: '',
    useStatus: '',
    inAble: '',
    outAble: '',
    area: '',
    isCrossZone: '',
    crossZoneArea: '',
    isWcs: '',
    wcsData: '',
    containerType: '',
    barcode: '',
    autoTransfer: '',
    status: '',
@@ -122,6 +127,25 @@
    outAble: 0,
    status: 1,
    memo: ''
  }
}
export function createBasStationInitState() {
  return {
    type: 0,
    useStatus: '',
    areaIds: [],
    containerTypes: [],
    inAble: 0,
    outAble: 0,
    rows: [createBasStationInitRow()]
  }
}
export function createBasStationInitRow() {
  return {
    stationName: '',
    stationId: ''
  }
}
@@ -203,6 +227,14 @@
        ? Number(params.type)
        : void 0,
    useStatus: normalizeText(params.useStatus),
    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,
    area:
      params.area !== undefined && params.area !== null && params.area !== ''
        ? Number(params.area)
@@ -211,13 +243,23 @@
      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:
      params.containerType !== undefined &&
      params.containerType !== null &&
      params.containerType !== ''
        ? Number(params.containerType)
        : void 0,
    barcode: normalizeText(params.barcode),
    autoTransfer:
      params.autoTransfer !== undefined && params.autoTransfer !== null && params.autoTransfer !== ''
      params.autoTransfer !== undefined &&
      params.autoTransfer !== null &&
      params.autoTransfer !== ''
        ? Number(params.autoTransfer)
        : void 0,
    status:
@@ -230,7 +272,9 @@
  }
  return Object.fromEntries(
    Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
    Object.entries(searchParams).filter(
      ([, value]) => value !== '' && value !== void 0 && value !== null
    )
  )
}
@@ -238,6 +282,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildBasStationSearchParams(params)
  }
}
@@ -256,10 +301,14 @@
      ? { area: Number(formData.area) }
      : {}),
    useStatus: normalizeText(formData.useStatus) || '',
    ...(formData.isCrossZone !== void 0 && formData.isCrossZone !== null && formData.isCrossZone !== ''
    ...(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) } : {}),
    ...(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) }
      : {}),
@@ -268,7 +317,9 @@
      ? { containerTypes: normalizePlainIdArray(formData.containerTypes) }
      : {}),
    barcode: normalizeText(formData.barcode) || '',
    ...(formData.autoTransfer !== void 0 && formData.autoTransfer !== null && formData.autoTransfer !== ''
    ...(formData.autoTransfer !== void 0 &&
    formData.autoTransfer !== null &&
    formData.autoTransfer !== ''
      ? { autoTransfer: Number(formData.autoTransfer) }
      : {}),
    ...(formData.inAble !== void 0 && formData.inAble !== null && formData.inAble !== ''
@@ -288,7 +339,9 @@
export function createBasStationDialogModel(record = {}) {
  return {
    ...createBasStationFormState(),
    ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
    ...(record.id !== void 0 && record.id !== null && record.id !== ''
      ? { id: Number(record.id) }
      : {}),
    stationName: normalizeText(record.stationName || ''),
    stationId: normalizeText(record.stationId || ''),
    type:
@@ -310,13 +363,18 @@
        ? Number(record.isWcs)
        : 0,
    wcsData: normalizeText(record.wcsData || ''),
    containerTypes: normalizePlainIdArray(record.containerTypes ?? record.containerType ?? record.containerTypes$ ?? []),
    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,
    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)
@@ -327,6 +385,74 @@
}
export const buildBasStationDialogModel = createBasStationDialogModel
export function buildBasStationInitModel(record = {}) {
  const initialRows = Array.isArray(record.rows) ? record.rows : record.pairs
  const rows =
    Array.isArray(initialRows) && initialRows.length
      ? initialRows
          .map((item) => ({
            stationName: normalizeText(item?.stationName || ''),
            stationId: normalizeText(item?.stationId || '')
          }))
          .filter((item) => item.stationName || item.stationId)
      : [createBasStationInitRow()]
  return {
    ...createBasStationInitState(),
    type:
      record.type !== void 0 && record.type !== null && record.type !== ''
        ? Number(record.type)
        : 0,
    useStatus: normalizeText(record.useStatus || ''),
    areaIds: normalizePlainIdArray(record.areaIds || []),
    containerTypes: normalizePlainIdArray(record.containerTypes || []),
    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,
    rows
  }
}
export function buildBasStationInitPayloadList(formData = {}) {
  const useStatus = normalizeText(formData.useStatus)
  const basePayload = {
    ...(formData.type !== void 0 && formData.type !== null && formData.type !== ''
      ? { type: Number(formData.type) }
      : {}),
    ...(useStatus ? { useStatus } : {}),
    ...(normalizePlainIdArray(formData.areaIds).length
      ? { areaIds: normalizePlainIdArray(formData.areaIds) }
      : {}),
    ...(normalizePlainIdArray(formData.containerTypes).length
      ? { containerTypes: normalizePlainIdArray(formData.containerTypes) }
      : {}),
    ...(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) }
      : {})
  }
  const rows = Array.isArray(formData.rows) ? formData.rows : []
  return rows
    .map((item) => ({
      stationName: normalizeText(item?.stationName || ''),
      stationId: normalizeText(item?.stationId || '')
    }))
    .filter((item) => item.stationName && item.stationId)
    .map((item) => ({
      ...basePayload,
      stationName: item.stationName,
      stationId: item.stationId
    }))
}
export function resolveBasStationAreaOptions(records = []) {
  if (!Array.isArray(records)) {
@@ -344,7 +470,9 @@
      }
      return {
        value: Number(value),
        label: normalizeText(item.name || item.areaName || item.code || item.areaCode || `库区 ${value}`)
        label: normalizeText(
          item.name || item.areaName || item.code || item.areaCode || `库区 ${value}`
        )
      }
    })
    .filter(Boolean)
@@ -403,7 +531,7 @@
      if (item === null || item === undefined || item === '') {
        return ''
      }
      const id = typeof item === 'object' ? item.id ?? item.areaId ?? item.value : item
      const id = typeof item === 'object' ? (item.id ?? item.areaId ?? item.value) : item
      if (typeof resolveLabel === 'function') {
        const resolvedLabel = normalizeText(resolveLabel(id))
        if (resolvedLabel) {
@@ -411,7 +539,9 @@
        }
      }
      if (typeof item === 'object') {
        return normalizeText(item.name || item.areaName || item.label || item.code || item.areaCode || id)
        return normalizeText(
          item.name || item.areaName || item.label || item.code || item.areaCode || id
        )
      }
      return normalizeText(`${fallbackPrefix} ${id}`)
    })
@@ -425,7 +555,7 @@
  }
  return records
    .map((item) => {
      const id = typeof item === 'object' ? item.id ?? item.value : item
      const id = typeof item === 'object' ? (item.id ?? item.value) : item
      if (typeof resolveLabel === 'function') {
        const resolved = normalizeText(resolveLabel(id))
        if (resolved) {
@@ -441,55 +571,108 @@
    .join('、')
}
export function normalizeBasStationDetailRecord(record = {}, resolveAreaLabel, resolveContainerTypeLabel) {
export function normalizeBasStationDetailRecord(
  record = {},
  resolveAreaLabel,
  resolveContainerTypeLabel
) {
  const areaIds = normalizeIdArray(record.areaIds ?? record.crossZoneArea ?? [])
  const containerTypes = normalizePlainIdArray(record.containerTypes ?? record.containerType ?? record.containerTypes$ ?? [])
  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)
  const crossZoneAreaItems = areaIds
    .map((item, index) => {
      const label = normalizeText(
        resolveAreaLabel?.(item.id) ||
          item.name ||
          item.areaName ||
          item.label ||
          item.code ||
          item.areaCode ||
          ''
      )
      return {
        id: item.id,
        sort: item.sort ?? index + 1,
        label: label || `库区 ${item.id}`
      }
    })
    .filter((item) => item.id !== void 0 && item.id !== null && item.id !== '')
  const containerTypeItems = containerTypes
    .map((item) => {
      const label = normalizeText(resolveContainerTypeLabel?.(item) || '')
      return {
        value: item,
        label: label || `容器类型 ${item}`
      }
    })
    .filter((item) => item.value !== void 0 && item.value !== null && item.value !== '')
  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,
    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 || ''),
    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,
    crossZoneAreaItems,
    crossZoneAreaText: buildIdLabelText(areaIds, resolveAreaLabel, '库区'),
    isCrossZone: record.isCrossZone !== void 0 && record.isCrossZone !== null && record.isCrossZone !== ''
      ? Number(record.isCrossZone)
      : void 0,
    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,
    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,
    containerTypeItems,
    containerTypesText: buildArrayText(containerTypes, resolveContainerTypeLabel, '容器类型'),
    barcode: normalizeText(record.barcode || ''),
    autoTransfer: record.autoTransfer !== void 0 && record.autoTransfer !== null && record.autoTransfer !== ''
      ? Number(record.autoTransfer)
      : void 0,
    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,
    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,
    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,
    stationAlias: Array.isArray(record.stationAlias)
      ? [...record.stationAlias]
      : record.stationAlias,
    stationAliasText: Array.isArray(record.stationAlias)
      ? record.stationAlias.map((item) => normalizeText(item)).filter(Boolean).join('、')
      ? record.stationAlias
          .map((item) => normalizeText(item))
          .filter(Boolean)
          .join('、')
      : normalizeText(record.stationAlias || record.stationAlias$ || ''),
    productionLineCode: normalizeText(record.productionLineCode || ''),
    productionLineName: normalizeText(record.productionLineName || ''),
@@ -501,15 +684,25 @@
  }
}
export function normalizeBasStationListRow(record = {}, resolveAreaLabel, resolveContainerTypeLabel) {
export function normalizeBasStationListRow(
  record = {},
  resolveAreaLabel,
  resolveContainerTypeLabel
) {
  return normalizeBasStationDetailRecord(record, resolveAreaLabel, resolveContainerTypeLabel)
}
export function buildBasStationPrintRows(records = [], resolveAreaLabel, resolveContainerTypeLabel) {
export function buildBasStationPrintRows(
  records = [],
  resolveAreaLabel,
  resolveContainerTypeLabel
) {
  if (!Array.isArray(records)) {
    return []
  }
  return records.map((record) => normalizeBasStationListRow(record, resolveAreaLabel, resolveContainerTypeLabel))
  return records.map((record) =>
    normalizeBasStationListRow(record, resolveAreaLabel, resolveContainerTypeLabel)
  )
}
export function buildBasStationReportMeta({
rsf-design/src/views/basic-info/bas-station/basStationTable.columns.js
@@ -2,6 +2,7 @@
import { ElTag } from 'element-plus'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
import { $t } from '@/locales'
import BasStationTagCell from './modules/bas-station-tag-cell.vue'
import {
  getBasStationUseStatusMeta,
  getBasStationStatusMeta,
@@ -10,6 +11,7 @@
export function createBasStationTableColumns({
  handleView,
  handleCopy,
  handleEdit,
  handleDelete,
  canEdit = true,
@@ -17,12 +19,21 @@
} = {}) {
  const operations = [{ key: 'view', label: $t('common.actions.detail'), icon: 'ri:eye-line' }]
  if (handleCopy) {
    operations.push({ key: 'copy', label: '复制初始化', icon: 'ri:file-copy-line' })
  }
  if (canEdit && handleEdit) {
    operations.push({ key: 'edit', label: $t('common.actions.edit'), icon: 'ri:pencil-line' })
  }
  if (canDelete && handleDelete) {
    operations.push({ key: 'delete', label: $t('common.actions.delete'), icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
    operations.push({
      key: 'delete',
      label: $t('common.actions.delete'),
      icon: 'ri:delete-bin-5-line',
      color: 'var(--art-error)'
    })
  }
  return [
@@ -70,18 +81,24 @@
      formatter: (row) => row.areaText || row.area$ || '--'
    },
    {
      prop: 'crossZoneAreaText',
      prop: 'crossZoneAreaItems',
      label: $t('pages.basicInfo.basStation.table.crossZoneArea'),
      minWidth: 220,
      showOverflowTooltip: true,
      formatter: (row) => row.crossZoneAreaText || '--'
      formatter: (row) =>
        h(BasStationTagCell, {
          items: row.crossZoneAreaItems || [],
          title: '可跨区库区'
        })
    },
    {
      prop: 'containerTypesText',
      prop: 'containerTypeItems',
      label: $t('pages.basicInfo.basStation.table.containerTypes'),
      minWidth: 200,
      showOverflowTooltip: true,
      formatter: (row) => row.containerTypesText || '--'
      formatter: (row) =>
        h(BasStationTagCell, {
          items: row.containerTypeItems || [],
          title: '可入容器类型'
        })
    },
    {
      prop: 'barcode',
@@ -150,6 +167,20 @@
      formatter: (row) => row.updateTimeText || row.updateTime$ || '--'
    },
    {
      prop: 'createByText',
      label: $t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) => row.createByText || row.createBy$ || '--'
    },
    {
      prop: 'createTimeText',
      label: $t('table.createTime'),
      minWidth: 170,
      showOverflowTooltip: true,
      formatter: (row) => row.createTimeText || row.createTime$ || '--'
    },
    {
      prop: 'memo',
      label: $t('table.remark'),
      minWidth: 180,
@@ -167,6 +198,7 @@
          list: operations,
          onClick: (item) => {
            if (item.key === 'view') handleView?.(row)
            if (item.key === 'copy') handleCopy?.(row)
            if (item.key === 'edit') handleEdit?.(row)
            if (item.key === 'delete') handleDelete?.(row)
          }
rsf-design/src/views/basic-info/bas-station/index.vue
@@ -13,6 +13,7 @@
        <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"
@@ -68,6 +69,15 @@
        :resolve-area-label="resolveAreaLabel"
        :resolve-container-type-label="resolveContainerTypeLabel"
      />
      <BasStationInitDialog
        v-model:visible="initDialogVisible"
        :initial-data="currentInitData"
        :area-options="areaOptions"
        :container-type-options="containerTypeOptions"
        :use-status-options="useStatusOptions"
        @submit="handleInitSubmit"
      />
    </ElCard>
  </div>
</template>
@@ -96,11 +106,14 @@
  } from '@/api/bas-station'
  import BasStationDialog from './modules/bas-station-dialog.vue'
  import BasStationDetailDrawer from './modules/bas-station-detail-drawer.vue'
  import BasStationInitDialog from './modules/bas-station-init-dialog.vue'
  import { createBasStationTableColumns } from './basStationTable.columns'
  import {
    BAS_STATION_REPORT_STYLE,
    BAS_STATION_REPORT_TITLE,
    buildBasStationDialogModel,
    buildBasStationInitModel,
    buildBasStationInitPayloadList,
    buildBasStationPageQueryParams,
    buildBasStationPrintRows,
    buildBasStationReportMeta,
@@ -129,6 +142,8 @@
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const detailData = ref({})
  const initDialogVisible = ref(false)
  const currentInitData = ref(buildBasStationInitModel())
  let handleDeleteAction = null
  const reportTitle = BAS_STATION_REPORT_TITLE
@@ -207,6 +222,24 @@
      }
    },
    {
      label: '可入',
      key: 'inAble',
      type: 'select',
      props: {
        clearable: true,
        options: getBasStationBooleanOptions()
      }
    },
    {
      label: '可出',
      key: 'outAble',
      type: 'select',
      props: {
        clearable: true,
        options: getBasStationBooleanOptions()
      }
    },
    {
      label: '所属库区',
      key: 'area',
      type: 'select',
@@ -226,12 +259,40 @@
      }
    },
    {
      label: '可跨区库区',
      key: 'crossZoneArea',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入可跨区库区'
      }
    },
    {
      label: '是否WCS',
      key: 'isWcs',
      type: 'select',
      props: {
        clearable: true,
        options: getBasStationBooleanOptions()
      }
    },
    {
      label: 'WCS数据',
      key: 'wcsData',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入WCS数据'
      }
    },
    {
      label: '容器类型',
      key: 'containerType',
      type: 'select',
      props: {
        clearable: true,
        filterable: true,
        options: containerTypeOptions.value
      }
    },
    {
@@ -301,10 +362,18 @@
    detailDrawerVisible.value = true
    detailLoading.value = true
    try {
      const detail = await guardRequestWithMessage(fetchGetBasStationDetail(row.id), {}, {
        timeoutMessage: '站点详情加载超时,已停止等待'
      })
      detailData.value = normalizeBasStationDetailRecord(detail, resolveAreaLabel, resolveContainerTypeLabel)
      const detail = await guardRequestWithMessage(
        fetchGetBasStationDetail(row.id),
        {},
        {
          timeoutMessage: '站点详情加载超时,已停止等待'
        }
      )
      detailData.value = normalizeBasStationDetailRecord(
        detail,
        resolveAreaLabel,
        resolveContainerTypeLabel
      )
    } catch (error) {
      detailDrawerVisible.value = false
      detailData.value = {}
@@ -316,39 +385,86 @@
  async function openEditDialog(row) {
    try {
      const detail = await guardRequestWithMessage(fetchGetBasStationDetail(row.id), {}, {
        timeoutMessage: '站点详情加载超时,已停止等待'
      })
      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))
  function createInitRecordFromRow(row) {
    return buildBasStationInitModel({
      type: row.type ?? 0,
      useStatus: row.useStatus || row.useStatusText || '',
      areaIds: Array.isArray(row.areaIds)
        ? row.areaIds.map((item) =>
            typeof item === 'object' ? (item.id ?? item.areaId ?? item.value) : item
          )
        : [],
      containerTypes: Array.isArray(row.containerTypes) ? [...row.containerTypes] : [],
      inAble: row.inAble ?? 0,
      outAble: row.outAble ?? 0,
      rows: [
        {
          stationName: row.stationName || '',
          stationId: row.stationId || ''
        }
      }
      ]
    })
  }
  function openInitDialog(record = null) {
    currentInitData.value = record ? createInitRecordFromRow(record) : buildBasStationInitModel()
    initDialogVisible.value = true
  }
  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,
          handleCopy: 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) =>
          normalizeBasStationListRow(item, resolveAreaLabel, resolveContainerTypeLabel)
        )
      }
    }
  })
  const {
    dialogVisible,
@@ -394,30 +510,39 @@
      await fetchBasStationPage({
        ...reportQueryParams.value,
        current: 1,
        pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
        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 {
    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
      orientation:
        previewMeta.value?.reportStyle?.orientation || BAS_STATION_REPORT_STYLE.orientation
    })
  )
@@ -429,6 +554,47 @@
  function handleReset() {
    Object.assign(searchForm.value, createBasStationSearchState())
    resetSearchParams()
  }
  async function handleInitSubmit(formData) {
    const payloads = buildBasStationInitPayloadList(formData)
    if (!payloads.length) {
      ElMessage.error('请至少填写一组站点编码和站点名称')
      return
    }
    let successCount = 0
    let failCount = 0
    let firstErrorMessage = ''
    for (const payload of payloads) {
      try {
        await fetchSaveBasStation(payload)
        successCount += 1
      } catch (error) {
        failCount += 1
        if (!firstErrorMessage) {
          firstErrorMessage = error?.message || `${payload.stationName} 初始化失败`
        }
      }
    }
    if (successCount > 0) {
      ElMessage.success(
        failCount > 0
          ? `成功保存 ${successCount} 条,失败 ${failCount} 条`
          : `成功保存 ${successCount} 条`
      )
      initDialogVisible.value = false
      currentInitData.value = buildBasStationInitModel()
      await refreshCreate()
      if (failCount > 0 && firstErrorMessage) {
        ElMessage.warning(firstErrorMessage)
      }
      return
    }
    ElMessage.error(firstErrorMessage || '站点初始化失败')
  }
  async function loadAreaOptions() {
@@ -449,7 +615,9 @@
      { records: [] },
      { timeoutMessage: '容器类型加载超时,已停止等待' }
    )
    containerTypeOptions.value = buildBasStationContainerTypeOptions(defaultResponseAdapter(response).records)
    containerTypeOptions.value = buildBasStationContainerTypeOptions(
      defaultResponseAdapter(response).records
    )
  }
  async function loadUseStatusOptions() {
@@ -463,7 +631,9 @@
      { records: [] },
      { timeoutMessage: '使用状态加载超时,已停止等待' }
    )
    useStatusOptions.value = buildBasStationUseStatusOptions(defaultResponseAdapter(response).records)
    useStatusOptions.value = buildBasStationUseStatusOptions(
      defaultResponseAdapter(response).records
    )
  }
  onMounted(async () => {
rsf-design/src/views/basic-info/bas-station/modules/bas-station-init-dialog.vue
New file
@@ -0,0 +1,239 @@
<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"
    />
    <div class="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="240">
          <template #default="{ row }">
            <ElInput v-model="row.stationName" clearable placeholder="请输入站点编码" />
          </template>
        </ElTableColumn>
        <ElTableColumn label="站点名称" min-width="240">
          <template #default="{ row }">
            <ElInput v-model="row.stationId" 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 {
    buildBasStationInitModel,
    createBasStationInitRow,
    createBasStationInitState,
    getBasStationBooleanOptions,
    getBasStationTypeOptions
  } from '../basStationPage.helpers'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    initialData: { 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(createBasStationInitState())
  const dialogTitle = computed(() => '站点初始化')
  const rules = computed(() => ({
    type: [{ 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: 'type',
      type: 'select',
      props: {
        placeholder: '请选择站点类型',
        clearable: true,
        options: getBasStationTypeOptions()
      }
    },
    {
      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: 'inAble',
      type: 'select',
      props: {
        placeholder: '请选择可入',
        clearable: true,
        options: getBasStationBooleanOptions()
      }
    },
    {
      label: '可出',
      key: 'outAble',
      type: 'select',
      props: {
        placeholder: '请选择可出',
        clearable: true,
        options: getBasStationBooleanOptions()
      }
    }
  ])
  function loadFormData() {
    Object.assign(form, buildBasStationInitModel(props.initialData))
  }
  function resetForm() {
    Object.assign(form, createBasStationInitState())
    form.rows = [createBasStationInitRow()]
    formRef.value?.clearValidate?.()
  }
  function addRow() {
    form.rows.push(createBasStationInitRow())
  }
  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 = (Array.isArray(form.rows) ? form.rows : []).filter(
        (item) => String(item?.stationName || '').trim() && String(item?.stationId || '').trim()
      )
      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 }
  )
</script>
rsf-design/src/views/basic-info/bas-station/modules/bas-station-tag-cell.vue
New file
@@ -0,0 +1,71 @@
<template>
  <div v-if="displayItems.length" class="tag-cell" @click="dialogVisible = true">
    <ElTag v-for="item in previewItems" :key="item.key" effect="plain" size="small">
      {{ item.label }}
    </ElTag>
    <ElTag v-if="hiddenCount > 0" effect="plain" size="small"> +{{ hiddenCount }} </ElTag>
  </div>
  <span v-else>{{ emptyText }}</span>
  <ElDialog v-model="dialogVisible" :title="title" width="720px" append-to-body destroy-on-close>
    <div v-if="displayItems.length" class="tag-cell__dialog-list">
      <ElTag v-for="item in displayItems" :key="item.key" effect="plain">
        {{ item.label }}
      </ElTag>
    </div>
    <ElEmpty v-else :description="emptyText" :image-size="88" />
  </ElDialog>
</template>
<script setup>
  import { computed, ref } from 'vue'
  const props = defineProps({
    items: { type: Array, default: () => [] },
    title: { type: String, default: '' },
    emptyText: { type: String, default: '--' },
    previewCount: { type: Number, default: 1 }
  })
  const dialogVisible = ref(false)
  const displayItems = computed(() =>
    (Array.isArray(props.items) ? props.items : [])
      .map((item, index) => {
        if (!item || typeof item !== 'object') {
          return null
        }
        const rawLabel = item.label ?? item.name ?? item.text ?? item.value ?? item.id
        const label = String(rawLabel ?? '').trim()
        if (!label) {
          return null
        }
        return {
          key: item.key ?? item.id ?? item.value ?? `${label}-${index}`,
          label
        }
      })
      .filter(Boolean)
  )
  const previewItems = computed(() => displayItems.value.slice(0, props.previewCount))
  const hiddenCount = computed(() =>
    Math.max(displayItems.value.length - previewItems.value.length, 0)
  )
</script>
<style scoped>
  .tag-cell {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    cursor: pointer;
  }
  .tag-cell__dialog-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }
</style>
rsf-design/src/views/orders/asn-order-log/asnOrderLogPage.helpers.js
@@ -165,6 +165,13 @@
    wkTypeText: normalizeTagText(record['wkType$'] || record.wkTypeText || record.wkType, {}),
    anfme: record.anfme ?? '--',
    qty: record.qty ?? '--',
    purchaseOrgName: normalizeText(record.purchaseOrgName) || '--',
    purchaseUserName: normalizeText(record.purchaseUserName) || '--',
    purchaseDateText:
      normalizeDateText(record.purchaseDate || record.businessTime || record['purchaseDate$']) ||
      '--',
    supplierId: normalizeText(record.supplierId) || '--',
    supplierName: normalizeText(record.supplierName || record['supplierId$']) || '--',
    logisNo: normalizeText(record.logisNo) || '--',
    arrTime: record.arrTime ?? null,
    arrTimeText: normalizeDateText(record['arrTime$'] || record.arrTime) || '--',
@@ -175,7 +182,10 @@
    ntyStatusText: normalizeTagText(record['ntyStatus$'] || ntyStatusMeta.text, NTY_STATUS_META),
    ntyStatusTagType: ntyStatusMeta.type,
    exceStatus: record.exceStatus ?? '--',
    exceStatusText: normalizeTagText(record['exceStatus$'] || record.exceStatusText || record.exceStatus, {}),
    exceStatusText: normalizeTagText(
      record['exceStatus$'] || record.exceStatusText || record.exceStatus,
      {}
    ),
    status: record.status ?? '--',
    statusText: normalizeTagText(record['status$'] || statusMeta.text, STATUS_META),
    statusType: statusMeta.type,
rsf-design/src/views/orders/asn-order-log/asnOrderLogTable.columns.js
@@ -69,6 +69,41 @@
      formatter: (row) => row.qty ?? '--'
    },
    {
      prop: 'purchaseOrgName',
      label: '采购组织',
      minWidth: 140,
      showOverflowTooltip: true,
      formatter: (row) => row.purchaseOrgName || '--'
    },
    {
      prop: 'purchaseUserName',
      label: '采购员',
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) => row.purchaseUserName || '--'
    },
    {
      prop: 'purchaseDateText',
      label: '采购日期',
      minWidth: 160,
      showOverflowTooltip: true,
      formatter: (row) => row.purchaseDateText || '--'
    },
    {
      prop: 'supplierId',
      label: '供应商编码',
      minWidth: 140,
      showOverflowTooltip: true,
      formatter: (row) => row.supplierId || '--'
    },
    {
      prop: 'supplierName',
      label: '供应商',
      minWidth: 160,
      showOverflowTooltip: true,
      formatter: (row) => row.supplierName || '--'
    },
    {
      prop: 'logisNo',
      label: $t('pages.orders.asnOrderLog.table.logisNo'),
      minWidth: 160,
@@ -125,6 +160,20 @@
      formatter: (row) => row.updateTimeText || '--'
    },
    {
      prop: 'createByText',
      label: $t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) => row.createByText || '--'
    },
    {
      prop: 'createTimeText',
      label: $t('table.createTime'),
      minWidth: 170,
      showOverflowTooltip: true,
      formatter: (row) => row.createTimeText || '--'
    },
    {
      prop: 'memo',
      label: $t('table.memo'),
      minWidth: 180,
rsf-design/src/views/orders/asn-order-log/index.vue
@@ -44,20 +44,14 @@
    <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"
      :log-id="activeLogId"
    />
  </div>
</template>
<script setup>
  import { computed, onMounted, reactive, ref } from 'vue'
  import { computed, onMounted, ref } from 'vue'
  import { ElMessage } from 'element-plus'
  import { useUserStore } from '@/store/modules/user'
  import { useTableColumns } from '@/hooks/core/useTableColumns'
@@ -68,7 +62,6 @@
  import { fetchDictDataPage } from '@/api/system-manage'
  import {
    DEFAULT_ASN_ORDER_LOG_PAGE_SIZE,
    buildAsnOrderLogDetailQueryParams,
    buildAsnOrderLogPageQueryParams,
    buildAsnOrderLogPrintRows,
    buildAsnOrderLogReportMeta,
@@ -77,24 +70,19 @@
    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'
  import { createAsnOrderLogTableColumns } from './asnOrderLogTable.columns'
  defineOptions({ name: 'AsnOrderLog' })
@@ -106,24 +94,15 @@
  const loading = ref(false)
  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 pageSize = ref(DEFAULT_ASN_ORDER_LOG_PAGE_SIZE)
  const cursorHistory = ref([null])
  const nextCursor = ref(null)
  const hasNext = ref(false)
  const detailPagination = reactive({
    current: 1,
    size: 20,
    total: 0
  })
  const reportQueryParams = computed(() => buildAsnOrderLogSearchParams(searchForm.value))
  const mainPaginationOptions = {
@@ -132,7 +111,8 @@
  }
  const pagination = computed(() => {
    const current = Math.max(1, cursorHistory.value.length || 1)
    const size = Number(pageSize.value) > 0 ? Number(pageSize.value) : DEFAULT_ASN_ORDER_LOG_PAGE_SIZE
    const size =
      Number(pageSize.value) > 0 ? Number(pageSize.value) : DEFAULT_ASN_ORDER_LOG_PAGE_SIZE
    const recordCount = Math.max(0, Number(data.value.length) || 0)
    const total = hasNext.value ? current * size + 1 : (current - 1) * size + recordCount
@@ -266,8 +246,7 @@
  async function loadMainList({ history = cursorHistory.value } = {}) {
    loading.value = true
    try {
      const normalizedHistory =
        Array.isArray(history) && history.length > 0 ? [...history] : [null]
      const normalizedHistory = Array.isArray(history) && history.length > 0 ? [...history] : [null]
      const response = await guardRequestWithMessage(
        fetchAsnOrderLogPage(
          buildAsnOrderLogPageQueryParams({
@@ -303,11 +282,11 @@
    }
  }
  function openDetail(row) {
  async function openDetail(row) {
    activeLogId.value = row.id
    detailPagination.current = 1
    detailData.value = normalizeAsnOrderLogRow(row || {})
    detailDrawerVisible.value = true
    loadDetailResources()
    await loadDetailResources()
  }
  async function loadDetailResources() {
@@ -316,47 +295,22 @@
    }
    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: '历史通知单明细加载超时,已停止等待'
          }
        )
      ])
      const detailResponse = await guardRequestWithMessage(
        fetchGetAsnOrderLogDetail(activeLogId.value),
        {},
        {
          timeoutMessage: '历史通知单详情加载超时,已停止等待'
        }
      )
      detailData.value = normalizeAsnOrderLogRow(detailResponse || {})
      const itemResult = defaultResponseAdapter(itemResponse)
      detailItemRows.value = Array.isArray(itemResult.records)
        ? itemResult.records.map((item) => normalizeAsnOrderItemLogRow(item))
        : []
      detailPagination.total = Number(itemResult?.total || 0)
      detailPagination.current = Number(itemResult?.current || detailPagination.current || 1)
      detailPagination.size = Number(itemResult?.size || detailPagination.size || 20)
    } catch (error) {
      detailDrawerVisible.value = false
      detailData.value = {}
      detailItemRows.value = []
      ElMessage.error(error?.message || '获取历史通知单详情失败')
    } finally {
      detailLoading.value = false
      detailItemsLoading.value = false
    }
  }
@@ -416,17 +370,6 @@
    await loadMainList()
  }
  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 {
@@ -461,7 +404,11 @@
      records.push(...pageRecords)
      if (!response?.hasNext || response?.nextCursor === null || response?.nextCursor === undefined) {
      if (
        !response?.hasNext ||
        response?.nextCursor === null ||
        response?.nextCursor === undefined
      ) {
        break
      }
      cursor = response.nextCursor
rsf-design/src/views/orders/asn-order-log/modules/asn-order-item-log-panel.vue
New file
@@ -0,0 +1,447 @@
<template>
  <div class="flex min-h-0 flex-1 flex-col gap-3">
    <ArtSearchBar
      v-model="searchForm"
      :items="searchItems"
      :showExpand="true"
      @search="handleSearch"
      @reset="handleReset"
    />
    <ElCard class="art-table-card min-h-0 flex-1">
      <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, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  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 '../asnOrderLogTable.columns'
  import { normalizeAsnOrderItemLogRow } from '../asnOrderLogPage.helpers'
  import {
    ASN_ORDER_ITEM_LOG_REPORT_STYLE,
    ASN_ORDER_ITEM_LOG_REPORT_TITLE,
    buildAsnOrderItemLogPageQueryParams,
    buildAsnOrderItemLogPrintRows,
    buildAsnOrderItemLogReportMeta,
    buildAsnOrderItemLogSearchParams,
    createAsnOrderItemLogSearchState,
    getAsnOrderItemLogPaginationKey,
    getAsnOrderItemLogReportColumns
  } from '../../asn-order-item-log/asnOrderItemLogPage.helpers'
  defineOptions({ name: 'AsnOrderItemLogPanel' })
  const props = defineProps({
    logId: {
      type: [Number, String],
      default: ''
    }
  })
  const { t } = useI18n()
  const userStore = useUserStore()
  const selectedRows = ref([])
  const searchForm = ref(createSeededSearchState())
  const reportTitle = ASN_ORDER_ITEM_LOG_REPORT_TITLE
  const reportColumns = getAsnOrderItemLogReportColumns()
  const searchItems = computed(() => [
    {
      label: t('pages.orders.asnOrderItemLog.search.condition'),
      key: 'condition',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.search.conditionPlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.asnCode'),
      key: 'orderCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.search.asnCodePlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.platItemId'),
      key: 'platItemId',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.platItemId')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.poDetlId'),
      key: 'poDetlId',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: t('pages.orders.asnOrderItemLog.table.poDetlId')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.poCode'),
      key: 'poCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.search.poCodePlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.fieldsIndex'),
      key: 'fieldsIndex',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.fieldsIndex')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.matnrCode'),
      key: 'matnrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.search.matnrCodePlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.maktx'),
      key: 'maktx',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.search.maktxPlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.anfme'),
      key: 'anfme',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: t('pages.orders.asnOrderItemLog.table.anfme')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.stockUnit'),
      key: 'stockUnit',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.stockUnit')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.purQty'),
      key: 'purQty',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: t('pages.orders.asnOrderItemLog.table.purQty')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.purUnit'),
      key: 'purUnit',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.purUnit')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.qty'),
      key: 'qty',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: t('pages.orders.asnOrderItemLog.table.qty')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.splrCode'),
      key: 'splrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.splrCode')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.splrBatch'),
      key: 'splrBatch',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.search.splrBatchPlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.splrName'),
      key: 'splrName',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.splrName')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.qrcode'),
      key: 'qrcode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.qrcode')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.trackCode'),
      key: 'trackCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.trackCode')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.barcode'),
      key: 'barcode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.barcode')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.packName'),
      key: 'packName',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItemLog.table.packName')
      }
    },
    {
      label: t('pages.orders.asnOrderItemLog.table.ntyStatus'),
      key: 'ntyStatus',
      type: 'select',
      props: {
        clearable: true,
        options: [
          { label: t('pages.orders.asnOrderItemLog.status.notReported'), value: 0 },
          { label: t('pages.orders.asnOrderItemLog.status.reported'), value: 1 },
          { label: t('pages.orders.asnOrderItemLog.status.partialReported'), value: 2 }
        ]
      }
    },
    {
      label: t('table.memo'),
      key: 'memo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('table.memo')
      }
    },
    {
      label: t('table.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: [
          { label: t('common.status.normal'), value: 1 },
          { label: t('common.status.frozen'), value: 0 }
        ]
      }
    }
  ])
  const reportQueryParams = computed(() =>
    buildAsnOrderItemLogSearchParams({
      ...searchForm.value,
      logId: normalizeLogId(props.logId)
    })
  )
  const {
    columns,
    columnChecks,
    data,
    loading,
    pagination,
    getData,
    replaceSearchParams,
    handleSizeChange,
    handleCurrentChange,
    refreshData
  } = useTable({
    core: {
      apiFn: fetchAsnOrderItemLogPage,
      apiParams: buildAsnOrderItemLogPageQueryParams(searchForm.value),
      paginationKey: getAsnOrderItemLogPaginationKey(),
      immediate: false,
      columnsFactory: () => createAsnOrderItemLogColumns()
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeAsnOrderItemLogRow(item)) : []
    }
  })
  watch(
    () => props.logId,
    (value) => {
      if (normalizeLogId(value) === '') {
        return
      }
      searchForm.value = createSeededSearchState()
      replaceSearchParams(buildAsnOrderItemLogPageQueryParams(searchForm.value))
      getData()
    },
    { immediate: true }
  )
  function normalizeLogId(value) {
    if (value === '' || value === null || value === undefined) {
      return ''
    }
    const parsed = Number(value)
    return Number.isFinite(parsed) ? parsed : ''
  }
  function createSeededSearchState() {
    return createAsnOrderItemLogSearchState({
      logId: normalizeLogId(props.logId)
    })
  }
  function buildQueryParams(params = {}) {
    return buildAsnOrderItemLogPageQueryParams({
      ...params,
      logId: normalizeLogId(props.logId)
    })
  }
  function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params,
      logId: normalizeLogId(props.logId)
    }
    replaceSearchParams(buildQueryParams(searchForm.value))
    getData()
  }
  function handleReset() {
    searchForm.value = createSeededSearchState()
    replaceSearchParams(buildQueryParams(searchForm.value))
    getData()
  }
  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,
        logId: normalizeLogId(props.logId),
        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: () =>
      fetchExportAsnOrderItemLogReport(
        {
          logId: normalizeLogId(props.logId)
        },
        {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }
      ),
    resolvePrintRecords,
    buildPreviewRows: (records) => buildAsnOrderItemLogPrintRows(records),
    buildPreviewMeta: (rows) => ({
      reportTitle,
      reportDate: new Date().toLocaleDateString(),
      printedAt: new Date().toLocaleString([], { 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>
rsf-design/src/views/orders/asn-order-log/modules/asn-order-log-detail-drawer.vue
@@ -14,57 +14,73 @@
          <ElDescriptionsItem label="PO单ID">{{ detail.poId ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="单据类型">{{ detail.typeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="业务类型">{{ detail.wkTypeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="采购组织">{{
            detail.purchaseOrgName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem label="采购员">{{
            detail.purchaseUserName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem label="采购日期">{{
            detail.purchaseDateText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem label="供应商编码">{{
            detail.supplierId || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem label="供应商">{{ detail.supplierName || '--' }}</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="预计到达时间">{{
            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>
            <ElTag :type="detail.statusType || 'info'" effect="light">{{
              detail.statusText || '--'
            }}</ElTag>
          </ElDescriptionsItem>
          <ElDescriptionsItem label="创建人">{{ detail.createByText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="创建时间">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="创建时间">{{
            detail.createTimeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem label="更新人">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="更新时间">{{ detail.updateTimeText || '--' }}</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>
        <AsnOrderItemLogPanel :log-id="logId" />
      </div>
    </ElScrollbar>
  </ElDrawer>
</template>
<script setup>
  import AsnOrderItemLogPanel from './asn-order-item-log-panel.vue'
  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 }) }
    logId: { type: [Number, String], default: '' }
  })
  const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
  const emit = defineEmits(['update:visible'])
  function handleVisibleChange(visible) {
    emit('update:visible', visible)
rsf-design/src/views/orders/asn-order/asnOrderPage.helpers.js
@@ -15,6 +15,10 @@
  return String(value ?? '').trim()
}
function normalizeDateValue(value) {
  return value ? String(value) : ''
}
function normalizeNumber(value) {
  if (value === '' || value === null || value === undefined) {
    return 0
@@ -60,7 +64,14 @@
    condition: '',
    code: '',
    poCode: '',
    poId: '',
    type: '',
    wkType: '',
    anfme: '',
    qty: '',
    logisNo: '',
    arrTime: '',
    memo: '',
    exceStatus: '',
    supplierName: '',
    purchaseUserName: ''
@@ -78,10 +89,27 @@
export function buildAsnOrderSearchParams(params = {}) {
  const result = {}
  ;['condition', 'code', 'poCode', 'wkType', 'supplierName', 'purchaseUserName'].forEach((key) => {
  ;[
    'condition',
    'code',
    'poCode',
    'type',
    'wkType',
    'logisNo',
    'arrTime',
    'memo',
    'supplierName',
    'purchaseUserName'
  ].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) {
      result[key] = value
    }
  })
  ;['poId', 'anfme', 'qty'].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) {
      result[key] = Number(value)
    }
  })
@@ -96,6 +124,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: params.orderBy || 'create_time desc',
    ...buildAsnOrderSearchParams(params)
  }
}
@@ -129,11 +158,13 @@
export function normalizeAsnOrderRow(record = {}, t = $t) {
  const statusConfig = getStatusConfig(record.exceStatus, record['exceStatus$'], t)
  const exceStatus = Number(record.exceStatus)
  return {
    ...record,
    id: record.id ?? null,
    code: record.code || '-',
    poCode: record.poCode || '-',
    poId: record.poId ?? '-',
    wkTypeLabel: record['wkType$'] || record.wkType || '-',
    orderTypeLabel: record['type$'] || record.type || '-',
    exceStatusText: statusConfig.label,
@@ -149,8 +180,12 @@
    updateTimeText: record['updateTime$'] || record.updateTime || '-',
    createByText: record['createBy$'] || '-',
    createTimeText: record['createTime$'] || record.createTime || '-',
    logisNo: record.logisNo || '-',
    arrTimeText: record['arrTime$'] || record.arrTime || '-',
    memo: record.memo || '-',
    canComplete: Number(record.exceStatus) === 1
    canEdit: exceStatus === 0 || exceStatus === 1,
    canDelete: exceStatus === 0,
    canComplete: exceStatus === 1
  }
}
@@ -173,6 +208,200 @@
    memo: record.memo || '-',
    prodTimeText: record['prodTime$'] || record.prodTime || '-'
  }
}
let tempItemSeed = 0
function createTempRowKey(prefix = 'asn-item') {
  tempItemSeed += 1
  return `${prefix}-${Date.now()}-${tempItemSeed}`
}
export function createAsnOrderFormState() {
  return {
    id: undefined,
    version: undefined,
    code: '',
    type: '',
    wkType: '',
    poCode: '',
    logisNo: '',
    arrTime: '',
    memo: ''
  }
}
export function buildAsnOrderDialogModel(record = {}) {
  return {
    ...createAsnOrderFormState(),
    ...record,
    poCode: normalizeText(record.poCode),
    logisNo: normalizeText(record.logisNo),
    arrTime: normalizeDateValue(record.arrTime || record['arrTime$']),
    memo: normalizeText(record.memo)
  }
}
export function createAsnOrderMaterialSearchState() {
  return {
    name: '',
    code: '',
    groupId: undefined
  }
}
export function buildAsnOrderMaterialSearchParams(params = {}) {
  const result = {}
  ;['name', 'code'].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) {
      result[key] = value
    }
  })
  const groupId = params.groupId
  if (groupId !== undefined && groupId !== null && groupId !== '') {
    result.groupId = groupId
  }
  return result
}
export function buildAsnOrderEditableItem(record = {}, fieldDefinitions = []) {
  const dynamicFields = Object.fromEntries(
    fieldDefinitions.map((item) => [
      item.fields,
      record[item.fields] ?? record.extendFields?.[item.fields] ?? ''
    ])
  )
  return {
    ...record,
    ...dynamicFields,
    __rowKey: record.__rowKey || (record.id ? `existing-${record.id}` : createTempRowKey()),
    matnrId: record.matnrId ?? null,
    matnrCode: record.matnrCode || '-',
    maktx: record.maktx || record.matnrName || '-',
    stockUnit: record.stockUnit || record.unit || '-',
    purUnit: record.purUnit || record.stockUnit || record.unit || '-',
    platItemId: record.platItemId || '',
    splrBatch: record.splrBatch || '',
    splrCode: record.splrCode || '',
    splrName: record.splrName || record.supplierName || '',
    memo: record.memo || '',
    anfme: normalizeNumber(record.anfme),
    qty: normalizeNumber(record.qty)
  }
}
export function createAsnOrderEditableItemFromMaterial(record = {}, fieldDefinitions = []) {
  const dynamicFields = Object.fromEntries(
    fieldDefinitions.map((item) => [
      item.fields,
      record[item.fields] ?? record.extendFields?.[item.fields] ?? ''
    ])
  )
  return {
    ...record,
    ...dynamicFields,
    __rowKey: createTempRowKey('asn-material'),
    id: undefined,
    matnrId: record.id ?? record.matnrId ?? null,
    matnrCode: record.code || record.matnrCode || '',
    maktx: record.name || record.maktx || '',
    stockUnit: record.stockUnit || record.unit || '',
    purUnit: record.purUnit || record.unit || '',
    platItemId: '',
    splrBatch: '',
    splrCode: '',
    splrName: '',
    memo: '',
    anfme: 0,
    qty: 0
  }
}
export function buildAsnOrderSavePayload({
  formData = {},
  itemRows = [],
  fieldDefinitions = []
} = {}) {
  const orders = {
    ...formData,
    type: normalizeText(formData.type),
    wkType: normalizeText(formData.wkType),
    poCode: normalizeText(formData.poCode),
    logisNo: normalizeText(formData.logisNo),
    arrTime: normalizeDateValue(formData.arrTime),
    memo: normalizeText(formData.memo)
  }
  const items = itemRows.map((row) => {
    const payload = { ...row }
    delete payload.__rowKey
    fieldDefinitions.forEach((item) => {
      const value = row[item.fields] ?? row.extendFields?.[item.fields]
      if (value !== undefined && value !== null && value !== '') {
        payload[item.fields] = value
      }
    })
    return payload
  })
  return {
    orders,
    items
  }
}
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 normalizeTreeOptions(records = [], level = 0) {
  if (!Array.isArray(records)) {
    return []
  }
  return records
    .map((item) => {
      if (!item || typeof item !== 'object') {
        return null
      }
      const children = normalizeTreeOptions(item.children || item.childrens || [], level + 1)
      return {
        value: item.id,
        label: `${' '.repeat(level * 2)}${item.name || item.label || item.code || item.id}`,
        children
      }
    })
    .filter(Boolean)
}
export function normalizePurchaseRow(record = {}) {
@@ -243,9 +472,9 @@
  }))
}
export function getAsnOrderActionList(row = {}) {
export function getAsnOrderActionList(row = {}, options = {}) {
  const normalizedRow = normalizeAsnOrderRow(row)
  return [
  const actionList = [
    {
      key: 'view',
      label: $t('pages.orders.asnOrder.actions.view'),
@@ -269,4 +498,25 @@
      disabled: !normalizedRow.canComplete
    }
  ]
  if (options.canEdit) {
    actionList.splice(2, 0, {
      key: 'edit',
      label: $t('pages.orders.asnOrder.actions.edit'),
      icon: 'ri:edit-line',
      disabled: !normalizedRow.canEdit
    })
  }
  if (options.canDelete) {
    actionList.push({
      key: 'delete',
      label: $t('pages.orders.asnOrder.actions.delete'),
      icon: 'ri:delete-bin-line',
      color: 'var(--el-color-danger)',
      disabled: !normalizedRow.canDelete
    })
  }
  return actionList
}
rsf-design/src/views/orders/asn-order/asnOrderTable.columns.js
@@ -5,7 +5,11 @@
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
import { getAsnOrderActionList } from './asnOrderPage.helpers'
export function createAsnOrderTableColumns({ handleActionClick }) {
export function createAsnOrderTableColumns({
  handleActionClick,
  canEdit = false,
  canDelete = false
}) {
  return [
    {
      type: 'selection',
@@ -29,6 +33,13 @@
      label: $t('pages.orders.asnOrder.search.poCode'),
      minWidth: 170,
      showOverflowTooltip: true
    },
    {
      prop: 'poId',
      label: $t('pages.orders.asnOrder.search.poId'),
      width: 110,
      align: 'right',
      visible: false
    },
    {
      prop: 'wkTypeLabel',
@@ -61,6 +72,27 @@
      showOverflowTooltip: true
    },
    {
      prop: 'purchaseOrgName',
      label: $t('pages.orders.asnOrder.table.purchaseOrgName'),
      minWidth: 140,
      showOverflowTooltip: true,
      visible: false
    },
    {
      prop: 'businessTimeText',
      label: $t('pages.orders.asnOrder.table.businessTime'),
      minWidth: 170,
      showOverflowTooltip: true,
      visible: false
    },
    {
      prop: 'supplierId',
      label: $t('pages.orders.asnOrder.table.supplierId'),
      minWidth: 140,
      showOverflowTooltip: true,
      visible: false
    },
    {
      prop: 'exceStatusText',
      label: $t('pages.orders.asnOrder.search.exceStatus'),
      width: 120,
@@ -72,20 +104,48 @@
        )
    },
    {
      prop: 'updateByText',
      label: $t('table.updateBy'),
      minWidth: 120,
      showOverflowTooltip: true,
      visible: false
    },
    {
      prop: 'updateTimeText',
      label: $t('pages.orders.asnOrder.detail.updateTime'),
      minWidth: 170,
      showOverflowTooltip: true
    },
    {
      prop: 'createByText',
      label: $t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true,
      visible: false
    },
    {
      prop: 'createTimeText',
      label: $t('pages.orders.asnOrder.detail.createTime'),
      minWidth: 170,
      showOverflowTooltip: true,
      visible: false
    },
    {
      prop: 'memo',
      label: $t('table.remark'),
      minWidth: 180,
      showOverflowTooltip: true,
      visible: false
    },
    {
      prop: 'operation',
      label: $t('table.operation'),
      width: 120,
      width: 130,
      align: 'center',
      fixed: 'right',
      formatter: (row) =>
        h(ArtButtonMore, {
          list: getAsnOrderActionList(row),
          list: getAsnOrderActionList(row, { canEdit, canDelete }),
          onClick: (item) => handleActionClick(item, row)
        })
    }
rsf-design/src/views/orders/asn-order/index.vue
@@ -12,8 +12,35 @@
      <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
        <template #left>
          <ElSpace wrap>
            <ElButton type="primary" @click="poDialogVisible = true">{{ t('pages.orders.asnOrder.buttons.createByPo') }}</ElButton>
            <ElButton v-auth="'add'" type="primary" @click="showDialog('add')">
              {{ t('pages.orders.asnOrder.buttons.create') }}
            </ElButton>
            <ElButton v-auth="'add'" @click="poDialogVisible = true">
              {{ t('pages.orders.asnOrder.buttons.createByPo') }}
            </ElButton>
            <ElUpload
              v-auth="'update'"
              :auto-upload="false"
              :show-file-list="false"
              accept=".xlsx,.xls"
              @change="handleImportFileChange"
            >
              <ElButton :loading="importing">
                {{ t('pages.orders.asnOrder.buttons.import') }}
              </ElButton>
            </ElUpload>
            <ElButton
              v-auth="'update'"
              :loading="templateDownloading"
              @click="handleDownloadTemplate"
            >
              {{ t('pages.orders.asnOrder.buttons.downloadTemplate') }}
            </ElButton>
            <ElButton :disabled="selectedRows.length === 0" @click="handleInspectSelected">
              {{ t('pages.orders.asnOrder.buttons.inspection') }}
            </ElButton>
            <ListExportPrint
              class="inline-flex"
              :preview-visible="previewVisible"
              @update:previewVisible="handlePreviewVisibleChange"
              :report-title="reportTitle"
@@ -42,6 +69,16 @@
      />
    </ElCard>
    <AsnOrderDialog
      v-model:visible="dialogVisible"
      :dialog-type="dialogType"
      :order-data="currentOrderData"
      :type-options="typeOptions"
      :wk-type-options="wkTypeOptions"
      :field-definitions="fieldDefinitions"
      @submit="handleDialogSubmit"
    />
    <AsnOrderDetailDrawer
      v-model:visible="detailDrawerVisible"
      :loading="detailLoading"
@@ -55,17 +92,20 @@
    />
    <AsnOrderCreateByPoDialog v-model:visible="poDialogVisible" @success="handlePoCreateSuccess" />
    <AsnOrderItemDialog v-model:visible="itemDialogVisible" :order="currentItemOrder" />
  </div>
</template>
<script setup>
  import { computed, reactive, ref } from 'vue'
  import { useRouter } from 'vue-router'
  import { computed, onMounted, reactive, ref } from 'vue'
  import { useI18n } from 'vue-i18n'
  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 { fetchDictDataPage } from '@/api/system-manage'
  import ListExportPrint from '@/components/biz/list-export-print/index.vue'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
@@ -73,11 +113,21 @@
    fetchAsnOrderItemPage,
    fetchAsnOrderPage,
    fetchCompleteAsnOrder,
    fetchDeleteAsnOrder,
    fetchDownloadAsnOrderTemplate,
    fetchEnabledAsnOrderFields,
    fetchExportAsnOrderReport,
    fetchGetAsnOrderMany
    fetchGetAsnOrderDetail,
    fetchGetAsnOrderMany,
    fetchImportAsnOrder,
    fetchInspectAsnOrder,
    fetchSaveAsnOrderWithItems,
    fetchUpdateAsnOrderWithItems
  } from '@/api/asn-order'
  import AsnOrderDialog from './modules/asn-order-dialog.vue'
  import AsnOrderDetailDrawer from './modules/asn-order-detail-drawer.vue'
  import AsnOrderCreateByPoDialog from './modules/asn-order-create-by-po-dialog.vue'
  import AsnOrderItemDialog from './modules/asn-order-item-dialog.vue'
  import {
    createAsnOrderDetailItemColumns,
    createAsnOrderTableColumns
@@ -85,22 +135,27 @@
  import {
    ASN_ORDER_REPORT_STYLE,
    buildAsnOrderDetailQueryParams,
    buildAsnOrderDialogModel,
    buildAsnOrderPageQueryParams,
    buildAsnOrderPrintRows,
    buildAsnOrderReportMeta,
    buildAsnOrderSavePayload,
    buildAsnOrderSearchParams,
    createAsnOrderFormState,
    createAsnOrderSearchState,
    getAsnOrderReportTitle,
    getAsnOrderStatusOptions,
    normalizeAsnOrderItemRow,
    normalizeAsnOrderRow
    normalizeAsnOrderRow,
    resolveDictOptions
  } from './asnOrderPage.helpers'
  defineOptions({ name: 'AsnOrder' })
  const userStore = useUserStore()
  const router = useRouter()
  const { hasAuth } = useAuth()
  const { t } = useI18n()
  const reportTitle = computed(() => getAsnOrderReportTitle(t))
  const searchForm = ref(createAsnOrderSearchState())
  const selectedRows = ref([])
@@ -111,6 +166,19 @@
  const activeOrderId = ref(null)
  const activeOrderRow = ref(null)
  const poDialogVisible = ref(false)
  const dialogVisible = ref(false)
  const itemDialogVisible = ref(false)
  const dialogType = ref('add')
  const currentOrderData = ref({
    order: createAsnOrderFormState(),
    items: []
  })
  const currentItemOrder = ref({})
  const typeOptions = ref([])
  const wkTypeOptions = ref([])
  const fieldDefinitions = ref([])
  const importing = ref(false)
  const templateDownloading = ref(false)
  const detailPagination = reactive({
    current: 1,
@@ -149,12 +217,81 @@
      }
    },
    {
      label: t('pages.orders.asnOrder.search.wkType'),
      key: 'wkType',
      label: t('pages.orders.asnOrder.search.poId'),
      key: 'poId',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.placeholder.poId')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.type'),
      key: 'type',
      type: 'select',
      props: {
        clearable: true,
        filterable: true,
        options: typeOptions.value,
        placeholder: t('pages.orders.asnOrder.placeholder.type')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.wkType'),
      key: 'wkType',
      type: 'select',
      props: {
        clearable: true,
        filterable: true,
        options: wkTypeOptions.value,
        placeholder: t('pages.orders.asnOrder.placeholder.wkType')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.anfme'),
      key: 'anfme',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.placeholder.anfme')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.qty'),
      key: 'qty',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.placeholder.qty')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.logisNo'),
      key: 'logisNo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.placeholder.logisNo')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.arrTime'),
      key: 'arrTime',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: t('pages.orders.asnOrder.placeholder.arrTime')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.memo'),
      key: 'memo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.placeholder.memo')
      }
    },
    {
@@ -186,6 +323,63 @@
    }
  ])
  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 createEmptyDialogData() {
    return {
      order: buildAsnOrderDialogModel(),
      items: []
    }
  }
  async function fetchAllOrderItems(orderId) {
    const firstPage = await guardRequestWithMessage(
      fetchAsnOrderItemPage({
        orderId,
        current: 1,
        pageSize: 200
      }),
      {
        records: [],
        total: 0,
        current: 1,
        size: 200
      },
      {
        timeoutMessage: t('pages.orders.asnOrder.messages.detailTimeout')
      }
    )
    const firstRecords = Array.isArray(firstPage?.records) ? firstPage.records : []
    const total = Number(firstPage?.total || firstRecords.length)
    if (total > firstRecords.length) {
      const fullPage = await guardRequestWithMessage(
        fetchAsnOrderItemPage({
          orderId,
          current: 1,
          pageSize: total
        }),
        {
          records: firstRecords,
          total,
          current: 1,
          size: total
        },
        {
          timeoutMessage: t('pages.orders.asnOrder.messages.detailTimeout')
        }
      )
      return Array.isArray(fullPage?.records) ? fullPage.records : firstRecords
    }
    return firstRecords
  }
  async function openDetail(row) {
    activeOrderId.value = row.id
    activeOrderRow.value = row
@@ -193,6 +387,50 @@
    detailPagination.current = 1
    detailDrawerVisible.value = true
    await loadDetailResources()
  }
  function showDialog(type, payload = createEmptyDialogData()) {
    dialogType.value = type
    currentOrderData.value = payload
    dialogVisible.value = true
  }
  async function openEditDialog(row) {
    try {
      const [detail, items] = await Promise.all([
        guardRequestWithMessage(
          fetchGetAsnOrderDetail(row.id),
          {},
          {
            timeoutMessage: t('pages.orders.asnOrder.messages.detailTimeout')
          }
        ),
        fetchAllOrderItems(row.id)
      ])
      showDialog('edit', {
        order: buildAsnOrderDialogModel(detail),
        items
      })
    } catch (error) {
      ElMessage.error(error?.message || t('pages.orders.asnOrder.messages.detailFailed'))
    }
  }
  async function handleDelete(row) {
    await ElMessageBox.confirm(
      t('pages.orders.asnOrder.messages.deleteConfirm', { code: row.code || '' }),
      t('pages.orders.asnOrder.messages.deleteTitle'),
      {
        confirmButtonText: t('common.confirm'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      }
    )
    await fetchDeleteAsnOrder(row.id)
    ElMessage.success(t('pages.orders.asnOrder.messages.deleteSuccess'))
    await refreshData()
  }
  async function handleActionClick(action, row) {
@@ -206,18 +444,24 @@
        return
      }
      if (action.key === 'edit') {
        await openEditDialog(row)
        return
      }
      if (action.key === 'print') {
        await handlePrint({ ids: [row.id], pageSize: 1 })
        return
      }
      if (action.key === 'items') {
        router.push({
          path: '/orders/asn-order-item',
          query: {
            orderId: String(row.id)
          }
        })
        currentItemOrder.value = row
        itemDialogVisible.value = true
        return
      }
      if (action.key === 'delete') {
        await handleDelete(row)
        return
      }
@@ -265,7 +509,9 @@
      apiParams: buildAsnOrderPageQueryParams(searchForm.value),
      columnsFactory: () =>
        createAsnOrderTableColumns({
          handleActionClick
          handleActionClick,
          canEdit: hasAuth('update'),
          canDelete: hasAuth('delete')
        })
    },
    transform: {
@@ -273,12 +519,6 @@
        Array.isArray(records) ? records.map((item) => normalizeAsnOrderRow(item, t)) : []
    }
  })
  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) {
@@ -354,6 +594,157 @@
    await refreshSoft()
  }
  async function handleDialogSubmit(payload) {
    try {
      const requestPayload = buildAsnOrderSavePayload({
        formData: payload.order,
        itemRows: payload.items,
        fieldDefinitions: fieldDefinitions.value
      })
      if (dialogType.value === 'edit') {
        await fetchUpdateAsnOrderWithItems(requestPayload)
        ElMessage.success(t('pages.orders.asnOrder.messages.updateSuccess'))
      } else {
        await fetchSaveAsnOrderWithItems(requestPayload)
        ElMessage.success(t('pages.orders.asnOrder.messages.createSuccess'))
      }
      dialogVisible.value = false
      currentOrderData.value = createEmptyDialogData()
      await refreshData()
    } catch (error) {
      ElMessage.error(
        error?.message ||
          (dialogType.value === 'edit'
            ? t('pages.orders.asnOrder.messages.updateFailed')
            : t('pages.orders.asnOrder.messages.createFailed'))
      )
    }
  }
  async function handleInspectSelected() {
    if (!selectedRows.value.length) {
      ElMessage.warning(t('pages.orders.asnOrder.messages.inspectionSelectRequired'))
      return
    }
    try {
      await ElMessageBox.confirm(
        t('pages.orders.asnOrder.messages.inspectionConfirm', { count: selectedRows.value.length }),
        t('pages.orders.asnOrder.messages.inspectionTitle'),
        {
          confirmButtonText: t('common.confirm'),
          cancelButtonText: t('common.cancel'),
          type: 'warning'
        }
      )
      await fetchInspectAsnOrder(selectedRows.value.map((row) => ({ id: row.id })))
      ElMessage.success(t('pages.orders.asnOrder.messages.inspectionSuccess'))
      await refreshData()
    } catch (error) {
      if (error === 'cancel' || error?.message === 'cancel') {
        return
      }
      ElMessage.error(error?.message || t('pages.orders.asnOrder.messages.inspectionFailed'))
    }
  }
  async function handleImportFileChange(uploadFile) {
    if (!uploadFile?.raw) {
      return
    }
    importing.value = true
    try {
      await fetchImportAsnOrder(uploadFile.raw)
      ElMessage.success(t('pages.orders.asnOrder.messages.importSuccess'))
      await refreshData()
    } catch (error) {
      ElMessage.error(error?.message || t('pages.orders.asnOrder.messages.importFailed'))
    } finally {
      importing.value = false
    }
  }
  async function downloadFile(response, fallbackName) {
    const blob = await response.blob()
    if (!blob || !blob.size) {
      throw new Error(t('pages.orders.asnOrder.messages.templateDownloadFailed'))
    }
    const disposition = response.headers.get('Content-Disposition') || ''
    const matchedName =
      disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1] ||
      disposition.match(/filename="?([^";]+)"?/i)?.[1]
    const fileName = matchedName ? decodeURIComponent(matchedName) : fallbackName
    const url = URL.createObjectURL(blob)
    const anchor = document.createElement('a')
    anchor.href = url
    anchor.download = fileName
    document.body.appendChild(anchor)
    anchor.click()
    anchor.remove()
    URL.revokeObjectURL(url)
  }
  async function handleDownloadTemplate() {
    templateDownloading.value = true
    try {
      const response = await fetchDownloadAsnOrderTemplate(
        {},
        {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }
      )
      await downloadFile(response, 'asn-order-template.xlsx')
      ElMessage.success(t('pages.orders.asnOrder.messages.templateDownloadSuccess'))
    } catch (error) {
      ElMessage.error(error?.message || t('pages.orders.asnOrder.messages.templateDownloadFailed'))
    } finally {
      templateDownloading.value = false
    }
  }
  async function loadTypeOptions() {
    const response = await guardRequestWithMessage(
      fetchDictDataPage({
        current: 1,
        pageSize: 200,
        dictTypeCode: 'sys_order_type',
        group: '1',
        status: 1
      }),
      { records: [] },
      { timeoutMessage: t('pages.orders.asnOrder.messages.typeOptionsTimeout') }
    )
    typeOptions.value = resolveDictOptions(defaultResponseAdapter(response).records, { group: '1' })
  }
  async function loadWkTypeOptions() {
    const response = await guardRequestWithMessage(
      fetchDictDataPage({
        current: 1,
        pageSize: 200,
        dictTypeCode: 'sys_business_type',
        group: '1',
        status: 1
      }),
      { records: [] },
      { timeoutMessage: t('pages.orders.asnOrder.messages.wkTypeOptionsTimeout') }
    )
    wkTypeOptions.value = resolveDictOptions(defaultResponseAdapter(response).records, {
      group: '1'
    })
  }
  async function loadFieldDefinitions() {
    const records = await guardRequestWithMessage(fetchEnabledAsnOrderFields(), [], {
      timeoutMessage: t('pages.orders.asnOrder.messages.fieldOptionsTimeout')
    })
    fieldDefinitions.value = Array.isArray(records) ? records : []
  }
  const {
    previewVisible,
    previewRows,
@@ -404,8 +795,13 @@
    buildAsnOrderReportMeta({
      previewMeta: previewMeta.value,
      count: previewRows.value.length,
      orientation: previewMeta.value?.reportStyle?.orientation || ASN_ORDER_REPORT_STYLE.orientation,
      orientation:
        previewMeta.value?.reportStyle?.orientation || ASN_ORDER_REPORT_STYLE.orientation,
      t
    })
  )
  onMounted(async () => {
    await Promise.allSettled([loadTypeOptions(), loadWkTypeOptions(), loadFieldDefinitions()])
  })
</script>
rsf-design/src/views/orders/asn-order/modules/asn-order-create-by-po-dialog.vue
@@ -20,8 +20,12 @@
        <ElCard class="art-table-card" shadow="never">
          <template #header>
            <div class="flex items-center justify-between gap-3">
              <div class="text-sm font-medium">{{ t('pages.orders.asnOrder.createByPoDialog.purchaseList') }}</div>
              <ElButton :loading="purchaseLoading" @click="refreshPurchaseData">{{ t('common.actions.refresh') }}</ElButton>
              <div class="text-sm font-medium">{{
                t('pages.orders.asnOrder.createByPoDialog.purchaseList')
              }}</div>
              <ElButton :loading="purchaseLoading" @click="refreshPurchaseData">{{
                t('common.actions.refresh')
              }}</ElButton>
            </div>
          </template>
@@ -39,11 +43,15 @@
          <template #header>
            <div class="flex items-center justify-between gap-3">
              <div class="flex flex-col">
                <span class="text-sm font-medium">{{ t('pages.orders.asnOrder.createByPoDialog.purchasePreview') }}</span>
                <span class="text-sm font-medium">{{
                  t('pages.orders.asnOrder.createByPoDialog.purchasePreview')
                }}</span>
                <span class="text-xs text-[var(--art-gray-600)]">
                  {{
                    selectedPurchase.code
                      ? t('pages.orders.asnOrder.createByPoDialog.purchaseSelected', { code: selectedPurchase.code })
                      ? t('pages.orders.asnOrder.createByPoDialog.purchaseSelected', {
                          code: selectedPurchase.code
                        })
                      : t('pages.orders.asnOrder.createByPoDialog.purchaseEmpty')
                  }}
                </span>
@@ -68,7 +76,10 @@
        <div class="text-xs text-[var(--art-gray-600)]">
          {{
            selectedPurchase.id
              ? t('pages.orders.asnOrder.createByPoDialog.purchaseGenerateHint', { code: selectedPurchase.code, count: purchaseItems.length })
              ? t('pages.orders.asnOrder.createByPoDialog.purchaseGenerateHint', {
                  code: selectedPurchase.code,
                  count: purchaseItems.length
                })
              : t('pages.orders.asnOrder.createByPoDialog.purchaseGenerateEmpty')
          }}
        </div>
@@ -252,7 +263,7 @@
          current: 1,
          size: 200
        },
          {
        {
          timeoutMessage: t('pages.orders.asnOrder.createByPoDialog.messages.purchaseItemsTimeout')
        }
      )
@@ -274,7 +285,9 @@
            size: total
          },
          {
            timeoutMessage: t('pages.orders.asnOrder.createByPoDialog.messages.purchaseItemsAllTimeout')
            timeoutMessage: t(
              'pages.orders.asnOrder.createByPoDialog.messages.purchaseItemsAllTimeout'
            )
          }
        )
        purchaseItems.value = Array.isArray(fullPage?.records)
@@ -333,7 +346,9 @@
      emit('success')
      handleVisibleChange(false)
    } catch (error) {
      ElMessage.error(error?.message || t('pages.orders.asnOrder.createByPoDialog.messages.createByPoFailed'))
      ElMessage.error(
        error?.message || t('pages.orders.asnOrder.createByPoDialog.messages.createByPoFailed')
      )
    } finally {
      submitLoading.value = false
    }
rsf-design/src/views/orders/asn-order/modules/asn-order-detail-drawer.vue
@@ -9,27 +9,59 @@
    <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
      <div class="space-y-4">
        <ElDescriptions :title="t('pages.orders.asnOrder.detail.baseInfo')" :column="4" border>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.asnCode')">{{ detail.code || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.poCode')">{{ detail.poCode || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.wkType')">{{ detail.wkTypeLabel || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.orderType')">{{ detail.orderTypeLabel || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.status')">{{ detail.exceStatusText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.purchaseOrg')">{{ detail.purchaseOrgName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.purchaseUser')">{{ detail.purchaseUserName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.supplier')">{{ detail.supplierName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.anfme')">{{ detail.anfme ?? 0 }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.qty')">{{ detail.qty ?? 0 }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.updateTime')">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.createTime')">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.memo')" :span="4">{{ detail.memo || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.asnCode')">{{
            detail.code || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.poCode')">{{
            detail.poCode || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.wkType')">{{
            detail.wkTypeLabel || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.orderType')">{{
            detail.orderTypeLabel || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.status')">{{
            detail.exceStatusText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.purchaseOrg')">{{
            detail.purchaseOrgName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.purchaseUser')">{{
            detail.purchaseUserName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.supplier')">{{
            detail.supplierName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.anfme')">{{
            detail.anfme ?? 0
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.qty')">{{
            detail.qty ?? 0
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.updateTime')">{{
            detail.updateTimeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.createTime')">{{
            detail.createTimeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.memo')" :span="4">{{
            detail.memo || '--'
          }}</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)]">{{ t('pages.orders.asnOrder.detail.items') }}</div>
            <div class="text-sm font-medium text-[var(--art-gray-900)]">{{
              t('pages.orders.asnOrder.detail.items')
            }}</div>
            <div class="flex items-center gap-3">
              <ElTag effect="plain">{{ t('pages.orders.asnOrder.detail.count', { count: data.length }) }}</ElTag>
              <ElButton :loading="loading" @click="$emit('refresh')">{{ t('common.actions.refresh') }}</ElButton>
              <ElTag effect="plain">{{
                t('pages.orders.asnOrder.detail.count', { count: data.length })
              }}</ElTag>
              <ElButton :loading="loading" @click="$emit('refresh')">{{
                t('common.actions.refresh')
              }}</ElButton>
            </div>
          </div>
rsf-design/src/views/orders/asn-order/modules/asn-order-dialog.vue
New file
@@ -0,0 +1,370 @@
<template>
  <ElDialog
    :model-value="visible"
    :title="dialogTitle"
    width="92%"
    top="4vh"
    destroy-on-close
    @update:model-value="handleVisibleChange"
    @closed="handleClosed"
  >
    <div class="flex flex-col gap-4">
      <ArtForm
        ref="formRef"
        v-model="form"
        :items="formItems"
        :rules="rules"
        :span="8"
        :gutter="20"
        label-width="110px"
        :show-reset="false"
        :show-submit="false"
      />
      <div class="flex flex-wrap items-center justify-between gap-3">
        <ElSpace wrap>
          <ElButton type="primary" @click="materialDialogVisible = true">
            {{ t('pages.orders.asnOrder.dialog.addMaterial') }}
          </ElButton>
          <ElButton
            type="danger"
            plain
            :disabled="selectedItemKeys.length === 0"
            @click="handleBatchRemove"
          >
            {{ t('pages.orders.asnOrder.dialog.deleteSelected') }}
          </ElButton>
        </ElSpace>
        <div class="text-xs text-[var(--art-gray-600)]">
          {{ t('pages.orders.asnOrder.dialog.itemCount', { count: itemRows.length }) }}
        </div>
      </div>
      <ElTable
        :data="itemRows"
        row-key="__rowKey"
        border
        size="small"
        max-height="420"
        @selection-change="handleItemSelectionChange"
      >
        <ElTableColumn type="selection" width="48" align="center" />
        <ElTableColumn type="index" :label="t('table.index')" width="72" align="center" />
        <ElTableColumn
          prop="matnrCode"
          :label="t('table.materialCode')"
          min-width="140"
          show-overflow-tooltip
        />
        <ElTableColumn
          prop="maktx"
          :label="t('table.materialName')"
          min-width="220"
          show-overflow-tooltip
        />
        <ElTableColumn
          :label="t('pages.orders.asnOrder.table.expectedQty')"
          width="140"
          align="right"
        >
          <template #default="{ row }">
            <ElInputNumber
              v-model="row.anfme"
              :min="0"
              :precision="2"
              controls-position="right"
              class="w-full"
            />
          </template>
        </ElTableColumn>
        <ElTableColumn :label="t('pages.orders.asnOrder.table.poItemId')" min-width="120">
          <template #default="{ row }">
            <ElInput v-model="row.platItemId" clearable />
          </template>
        </ElTableColumn>
        <ElTableColumn :label="t('table.supplierBatch')" min-width="140">
          <template #default="{ row }">
            <ElInput v-model="row.splrBatch" clearable />
          </template>
        </ElTableColumn>
        <ElTableColumn prop="stockUnit" :label="t('table.unit')" width="100" align="center" />
        <ElTableColumn
          v-for="field in fieldDefinitions"
          :key="field.fields"
          :label="field.fieldsAlise || field.fields"
          min-width="140"
        >
          <template #default="{ row }">
            <ElInput v-model="row[field.fields]" clearable />
          </template>
        </ElTableColumn>
        <ElTableColumn :label="t('table.operation')" fixed="right" width="88" align="center">
          <template #default="{ row }">
            <ElButton link type="danger" @click="handleRemoveRow(row)">
              {{ t('pages.orders.asnOrder.actions.delete') }}
            </ElButton>
          </template>
        </ElTableColumn>
      </ElTable>
    </div>
    <template #footer>
      <ElSpace>
        <ElButton @click="handleVisibleChange(false)">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" @click="handleSubmit">{{ t('common.confirm') }}</ElButton>
      </ElSpace>
    </template>
    <AsnOrderMaterialDialog
      v-model:visible="materialDialogVisible"
      :field-definitions="fieldDefinitions"
      :selected-matnr-ids="selectedMatnrIds"
      @confirm="handleMaterialConfirm"
    />
  </ElDialog>
</template>
<script setup>
  import { computed, nextTick, reactive, ref, watch } from 'vue'
  import { ElMessage } from 'element-plus'
  import { useI18n } from 'vue-i18n'
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import {
    buildAsnOrderDialogModel,
    buildAsnOrderEditableItem,
    createAsnOrderEditableItemFromMaterial,
    createAsnOrderFormState
  } from '../asnOrderPage.helpers'
  import AsnOrderMaterialDialog from './asn-order-material-dialog.vue'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    dialogType: { type: String, default: 'add' },
    orderData: {
      type: Object,
      default: () => ({
        order: {},
        items: []
      })
    },
    typeOptions: { type: Array, default: () => [] },
    wkTypeOptions: { type: Array, default: () => [] },
    fieldDefinitions: { type: Array, default: () => [] }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const { t } = useI18n()
  const formRef = ref()
  const form = reactive(createAsnOrderFormState())
  const itemRows = ref([])
  const selectedItemKeys = ref([])
  const materialDialogVisible = ref(false)
  const isEdit = computed(() => props.dialogType === 'edit')
  const dialogTitle = computed(() =>
    isEdit.value
      ? t('pages.orders.asnOrder.dialog.editTitle')
      : t('pages.orders.asnOrder.dialog.createTitle')
  )
  const selectedMatnrIds = computed(() =>
    itemRows.value.map((item) => item.matnrId).filter((item) => item !== undefined && item !== null)
  )
  const rules = computed(() => ({
    type: [
      {
        required: true,
        message: t('pages.orders.asnOrder.dialog.validation.type'),
        trigger: 'change'
      }
    ],
    wkType: [
      {
        required: true,
        message: t('pages.orders.asnOrder.dialog.validation.wkType'),
        trigger: 'change'
      }
    ]
  }))
  const formItems = computed(() => [
    {
      label: t('pages.orders.asnOrder.detail.orderType'),
      key: 'type',
      type: 'select',
      props: {
        clearable: true,
        filterable: true,
        placeholder: t('pages.orders.asnOrder.dialog.placeholder.type'),
        options: props.typeOptions
      }
    },
    {
      label: t('pages.orders.asnOrder.detail.wkType'),
      key: 'wkType',
      type: 'select',
      props: {
        clearable: true,
        filterable: true,
        placeholder: t('pages.orders.asnOrder.dialog.placeholder.wkType'),
        options: props.wkTypeOptions
      }
    },
    {
      label: t('pages.orders.asnOrder.detail.poCode'),
      key: 'poCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.dialog.placeholder.poCode')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.logisNo'),
      key: 'logisNo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.dialog.placeholder.logisNo')
      }
    },
    {
      label: t('pages.orders.asnOrder.search.arrTime'),
      key: 'arrTime',
      type: 'datetime',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD HH:mm:ss',
        format: 'YYYY-MM-DD HH:mm:ss',
        placeholder: t('pages.orders.asnOrder.dialog.placeholder.arrTime')
      }
    },
    {
      label: t('pages.orders.asnOrder.detail.memo'),
      key: 'memo',
      type: 'input',
      span: 24,
      props: {
        type: 'textarea',
        rows: 3,
        clearable: true,
        placeholder: t('pages.orders.asnOrder.dialog.placeholder.memo')
      }
    }
  ])
  function loadDialogData() {
    Object.assign(form, buildAsnOrderDialogModel(props.orderData?.order || {}))
    itemRows.value = Array.isArray(props.orderData?.items)
      ? props.orderData.items.map((item) => buildAsnOrderEditableItem(item, props.fieldDefinitions))
      : []
    selectedItemKeys.value = []
  }
  function resetDialogData() {
    Object.assign(form, createAsnOrderFormState())
    itemRows.value = []
    selectedItemKeys.value = []
    formRef.value?.clearValidate?.()
  }
  function handleItemSelectionChange(rows) {
    selectedItemKeys.value = Array.isArray(rows) ? rows.map((item) => item.__rowKey) : []
  }
  function handleRemoveRow(row) {
    itemRows.value = itemRows.value.filter((item) => item.__rowKey !== row.__rowKey)
    selectedItemKeys.value = selectedItemKeys.value.filter((item) => item !== row.__rowKey)
  }
  function handleBatchRemove() {
    if (!selectedItemKeys.value.length) {
      return
    }
    const selectedKeySet = new Set(selectedItemKeys.value)
    itemRows.value = itemRows.value.filter((item) => !selectedKeySet.has(item.__rowKey))
    selectedItemKeys.value = []
  }
  function handleMaterialConfirm(rows) {
    const existingMatnrIds = new Set(selectedMatnrIds.value)
    const nextRows = rows
      .map((item) => createAsnOrderEditableItemFromMaterial(item, props.fieldDefinitions))
      .filter((item) => !existingMatnrIds.has(item.matnrId))
    if (!nextRows.length) {
      ElMessage.warning(t('pages.orders.asnOrder.dialog.materialDuplicate'))
      return
    }
    itemRows.value = [...itemRows.value, ...nextRows]
  }
  function validateItems() {
    if (!itemRows.value.length) {
      ElMessage.warning(t('pages.orders.asnOrder.dialog.validation.items'))
      return false
    }
    const invalidRow = itemRows.value.find((item) => Number(item.anfme) <= 0)
    if (invalidRow) {
      ElMessage.warning(t('pages.orders.asnOrder.dialog.validation.anfme'))
      return false
    }
    return true
  }
  async function handleSubmit() {
    if (!formRef.value) {
      return
    }
    try {
      await formRef.value.validate()
    } catch {
      return
    }
    if (!validateItems()) {
      return
    }
    emit('submit', {
      order: { ...form },
      items: itemRows.value.map((item) => ({ ...item }))
    })
  }
  function handleVisibleChange(visible) {
    emit('update:visible', visible)
  }
  function handleClosed() {
    resetDialogData()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (visible) {
        loadDialogData()
        nextTick(() => {
          formRef.value?.clearValidate?.()
        })
      }
    },
    { immediate: true }
  )
  watch(
    () => props.orderData,
    () => {
      if (props.visible) {
        loadDialogData()
      }
    },
    { deep: true }
  )
</script>
rsf-design/src/views/orders/asn-order/modules/asn-order-item-dialog.vue
New file
@@ -0,0 +1,342 @@
<template>
  <ElDialog
    :model-value="visible"
    :title="t('menus.orders.asnOrderItem')"
    width="92vw"
    top="4vh"
    destroy-on-close
    append-to-body
    @update:model-value="handleVisibleChange"
  >
    <div class="flex h-[78vh] min-h-0 flex-col gap-3">
      <ElCard shadow="never" class="shrink-0">
        <ElDescriptions :column="4" border>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.asnCode')">
            {{ order.code || t('common.placeholder.empty') }}
          </ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.poCode')">
            {{ order.poCode || t('common.placeholder.empty') }}
          </ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.wkType')">
            {{ order.wkTypeLabel || t('common.placeholder.empty') }}
          </ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.asnOrder.detail.status')">
            {{ order.exceStatusText || t('common.placeholder.empty') }}
          </ElDescriptionsItem>
        </ElDescriptions>
      </ElCard>
      <ArtSearchBar
        v-model="searchForm"
        class="shrink-0"
        :items="searchItems"
        :showExpand="true"
        @search="handleSearch"
        @reset="handleReset"
      />
      <ElCard class="art-table-card min-h-0 flex-1">
        <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>
    </div>
    <template #footer>
      <ElButton @click="handleVisibleChange(false)">{{ t('common.cancel') }}</ElButton>
    </template>
    <AsnOrderItemDetailDrawer
      v-model:visible="detailDrawerVisible"
      :loading="detailLoading"
      :detail="detailData"
    />
  </ElDialog>
</template>
<script setup>
  import { ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import { ElMessage } from 'element-plus'
  import { useTable } from '@/hooks/core/useTable'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { fetchAsnOrderItemFullPage, fetchGetAsnOrderItemDetail } from '@/api/asn-order-item'
  import AsnOrderItemDetailDrawer from '../../asn-order-item/modules/asn-order-item-detail-drawer.vue'
  import { createAsnOrderItemTableColumns } from '../../asn-order-item/asnOrderItemTable.columns'
  import {
    buildAsnOrderItemPageQueryParams,
    createAsnOrderItemSearchState,
    normalizeAsnOrderItemDetail,
    normalizeAsnOrderItemRow
  } from '../../asn-order-item/asnOrderItemPage.helpers'
  defineOptions({ name: 'AsnOrderItemDialog' })
  const DEFAULT_PAGE_SIZE = 20
  const props = defineProps({
    visible: { type: Boolean, default: false },
    order: { type: Object, default: () => ({}) }
  })
  const emit = defineEmits(['update:visible'])
  const { t } = useI18n()
  const searchForm = ref(createDialogSearchState())
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const detailData = ref({})
  const activeItemId = ref(null)
  const searchItems = computed(() => [
    {
      label: t('table.keyword'),
      key: 'condition',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItem.search.conditionPlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItem.search.poCode'),
      key: 'poCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItem.search.poCodePlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItem.search.platWorkCode'),
      key: 'platWorkCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItem.search.platWorkCodePlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItem.search.platItemId'),
      key: 'platItemId',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItem.search.platItemIdPlaceholder')
      }
    },
    {
      label: t('table.materialCode'),
      key: 'matnrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItem.search.matnrCodePlaceholder')
      }
    },
    {
      label: t('table.materialName'),
      key: 'maktx',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItem.search.maktxPlaceholder')
      }
    },
    {
      label: t('table.supplierBatch'),
      key: 'splrBatch',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItem.search.splrBatchPlaceholder')
      }
    },
    {
      label: t('pages.orders.asnOrderItem.search.stockUnit'),
      key: 'stockUnit',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrderItem.search.stockUnitPlaceholder')
      }
    },
    {
      label: t('table.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: [
          { label: t('common.status.normal'), value: 1 },
          { label: t('common.status.frozen'), value: 0 }
        ]
      }
    },
    {
      label: t('pages.orders.asnOrderItem.search.ntyStatus'),
      key: 'ntyStatus',
      type: 'select',
      props: {
        clearable: true,
        options: [
          { label: t('pages.orders.asnOrderItem.ntyStatus.notReported'), value: 0 },
          { label: t('pages.orders.asnOrderItem.ntyStatus.reported'), value: 1 }
        ]
      }
    },
    {
      label: t('pages.orders.asnOrderItem.search.createTimeRange'),
      key: 'createTimeRange',
      type: 'datetimerange',
      props: {
        clearable: true,
        startPlaceholder: t('pages.orders.asnOrderItem.search.startTime'),
        endPlaceholder: t('pages.orders.asnOrderItem.search.endTime'),
        rangeSeparator: t('pages.orders.asnOrderItem.search.rangeSeparator')
      }
    },
    {
      label: t('pages.orders.asnOrderItem.search.updateTimeRange'),
      key: 'updateTimeRange',
      type: 'datetimerange',
      props: {
        clearable: true,
        startPlaceholder: t('pages.orders.asnOrderItem.search.startTime'),
        endPlaceholder: t('pages.orders.asnOrderItem.search.endTime'),
        rangeSeparator: t('pages.orders.asnOrderItem.search.rangeSeparator')
      }
    }
  ])
  const {
    columns,
    columnChecks,
    data,
    loading,
    pagination,
    replaceSearchParams,
    handleSizeChange,
    handleCurrentChange,
    refreshData,
    getData
  } = useTable({
    core: {
      apiFn: fetchAsnOrderItemFullPage,
      apiParams: buildQueryParams({
        ...searchForm.value,
        pageSize: DEFAULT_PAGE_SIZE
      }),
      immediate: false,
      columnsFactory: () =>
        createAsnOrderItemTableColumns({
          handleView: openDetail,
          t
        })
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeAsnOrderItemRow(item, t)) : []
    }
  })
  watch(
    () => [props.visible, props.order?.id],
    ([visible, orderId]) => {
      if (!visible || !orderId) {
        return
      }
      resetDialogState()
      replaceSearchParams(buildQueryParams(searchForm.value))
      getData()
    },
    { immediate: true }
  )
  function createDialogSearchState() {
    return {
      ...createAsnOrderItemSearchState(),
      orderId: props.order?.id ? String(props.order.id) : ''
    }
  }
  function buildQueryParams(params = {}) {
    return buildAsnOrderItemPageQueryParams({
      ...params,
      orderId: props.order?.id ? String(props.order.id) : '',
      orderBy: 'id desc'
    })
  }
  function resetDialogState() {
    searchForm.value = createDialogSearchState()
    detailDrawerVisible.value = false
    detailData.value = {}
    activeItemId.value = null
  }
  function handleVisibleChange(value) {
    emit('update:visible', value)
  }
  function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params,
      orderId: props.order?.id ? String(props.order.id) : ''
    }
    replaceSearchParams(buildQueryParams(searchForm.value))
    getData()
  }
  function handleReset() {
    resetDialogState()
    replaceSearchParams(buildQueryParams(searchForm.value))
    getData()
  }
  async function openDetail(row) {
    activeItemId.value = row.id
    detailData.value = normalizeAsnOrderItemDetail(row, t)
    detailDrawerVisible.value = true
    await loadDetailResource()
  }
  async function loadDetailResource() {
    if (!activeItemId.value) {
      return
    }
    detailLoading.value = true
    try {
      const detailResponse = await guardRequestWithMessage(
        fetchGetAsnOrderItemDetail(activeItemId.value),
        {},
        {
          timeoutMessage: t('pages.orders.asnOrderItem.messages.detailTimeout')
        }
      )
      detailData.value = normalizeAsnOrderItemDetail(
        {
          ...detailData.value,
          ...detailResponse
        },
        t
      )
    } catch (error) {
      detailDrawerVisible.value = false
      detailData.value = {}
      ElMessage.error(error?.message || t('pages.orders.asnOrderItem.messages.detailFailed'))
    } finally {
      detailLoading.value = false
    }
  }
</script>
rsf-design/src/views/orders/asn-order/modules/asn-order-material-dialog.vue
New file
@@ -0,0 +1,269 @@
<template>
  <ElDialog
    :model-value="visible"
    :title="t('pages.orders.asnOrder.materialDialog.title')"
    width="88%"
    top="5vh"
    destroy-on-close
    @update:model-value="handleVisibleChange"
  >
    <div class="flex flex-col gap-4">
      <ArtSearchBar
        v-model="searchForm"
        :items="searchItems"
        :showExpand="false"
        @search="handleSearch"
        @reset="handleReset"
      />
      <ArtTable
        :loading="loading"
        :data="data"
        :columns="columns"
        :pagination="pagination"
        @selection-change="handleSelectionChange"
        @pagination:size-change="handleSizeChange"
        @pagination:current-change="handleCurrentChange"
      />
    </div>
    <template #footer>
      <ElSpace>
        <ElButton @click="handleVisibleChange(false)">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" @click="handleConfirm">{{ t('common.confirm') }}</ElButton>
      </ElSpace>
    </template>
  </ElDialog>
</template>
<script setup>
  import { computed, h, ref, watch } from 'vue'
  import { ElTag } from 'element-plus'
  import { useI18n } from 'vue-i18n'
  import { useTable } from '@/hooks/core/useTable'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { fetchMatnrGroupTree, fetchMatnrPage } from '@/api/wh-mat'
  import {
    buildAsnOrderMaterialSearchParams,
    createAsnOrderMaterialSearchState,
    normalizeTreeOptions
  } from '../asnOrderPage.helpers'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    fieldDefinitions: { type: Array, default: () => [] },
    selectedMatnrIds: { type: Array, default: () => [] }
  })
  const emit = defineEmits(['update:visible', 'confirm'])
  const { t } = useI18n()
  const searchForm = ref(createAsnOrderMaterialSearchState())
  const selectedRows = ref([])
  const groupTreeOptions = ref([])
  const searchItems = computed(() => [
    {
      label: t('pages.orders.asnOrder.materialDialog.search.name'),
      key: 'name',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.materialDialog.placeholder.name')
      }
    },
    {
      label: t('pages.orders.asnOrder.materialDialog.search.code'),
      key: 'code',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.asnOrder.materialDialog.placeholder.code')
      }
    },
    {
      label: t('pages.orders.asnOrder.materialDialog.search.groupId'),
      key: 'groupId',
      type: 'treeselect',
      props: {
        clearable: true,
        checkStrictly: true,
        renderAfterExpand: false,
        showCheckbox: false,
        data: groupTreeOptions.value,
        placeholder: t('pages.orders.asnOrder.materialDialog.placeholder.groupId')
      }
    }
  ])
  const createColumns = () => {
    const baseColumns = [
      { type: 'selection', width: 48, align: 'center' },
      { type: 'globalIndex', label: t('table.index'), width: 72, align: 'center' },
      {
        prop: 'code',
        label: t('pages.orders.asnOrder.materialDialog.table.code'),
        minWidth: 170,
        showOverflowTooltip: true
      },
      {
        prop: 'name',
        label: t('pages.orders.asnOrder.materialDialog.table.name'),
        minWidth: 220,
        showOverflowTooltip: true
      },
      {
        prop: 'groupName',
        label: t('pages.orders.asnOrder.materialDialog.table.group'),
        minWidth: 140,
        showOverflowTooltip: true
      },
      {
        prop: 'spec',
        label: t('pages.orders.asnOrder.materialDialog.table.spec'),
        minWidth: 120,
        showOverflowTooltip: true,
        visible: false
      },
      {
        prop: 'model',
        label: t('pages.orders.asnOrder.materialDialog.table.model'),
        minWidth: 120,
        showOverflowTooltip: true,
        visible: false
      },
      {
        prop: 'unit',
        label: t('table.unit'),
        width: 100,
        align: 'center'
      },
      {
        prop: 'selected',
        label: t('pages.orders.asnOrder.materialDialog.table.status'),
        width: 110,
        formatter: (row) =>
          h(
            ElTag,
            {
              type: props.selectedMatnrIds.includes(row.id) ? 'warning' : 'info',
              effect: 'light'
            },
            () =>
              props.selectedMatnrIds.includes(row.id)
                ? t('pages.orders.asnOrder.materialDialog.selected')
                : t('pages.orders.asnOrder.materialDialog.unselected')
          )
      }
    ]
    return [
      ...baseColumns,
      ...props.fieldDefinitions.map((item) => ({
        prop: item.fields,
        label: item.fieldsAlise || item.fields,
        minWidth: 140,
        showOverflowTooltip: true,
        visible: false
      }))
    ]
  }
  const {
    data,
    loading,
    pagination,
    columns,
    replaceSearchParams,
    handleSizeChange,
    handleCurrentChange,
    getData,
    resetColumns
  } = useTable({
    core: {
      apiFn: fetchMatnrPage,
      apiParams: buildAsnOrderMaterialSearchParams(searchForm.value),
      immediate: false,
      columnsFactory: createColumns
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records)
          ? records.map((item) => ({
              ...item,
              groupName: item['groupId$'] || item.groupName || '-',
              unit: item.unit || item.stockUnit || '-',
              ...Object.fromEntries(
                props.fieldDefinitions.map((field) => [
                  field.fields,
                  item[field.fields] ?? item.extendFields?.[field.fields] ?? ''
                ])
              )
            }))
          : []
    }
  })
  function handleSelectionChange(rows) {
    selectedRows.value = Array.isArray(rows) ? rows : []
  }
  function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params
    }
    replaceSearchParams(buildAsnOrderMaterialSearchParams(searchForm.value))
    getData()
  }
  function handleReset() {
    searchForm.value = createAsnOrderMaterialSearchState()
    replaceSearchParams(buildAsnOrderMaterialSearchParams(searchForm.value))
    getData()
  }
  function handleConfirm() {
    emit(
      'confirm',
      selectedRows.value.map((item) => ({ ...item }))
    )
    handleVisibleChange(false)
  }
  function handleVisibleChange(visible) {
    emit('update:visible', visible)
  }
  async function loadGroupTree() {
    const response = await guardRequestWithMessage(fetchMatnrGroupTree({}), [], {
      timeoutMessage: t('pages.orders.asnOrder.materialDialog.messages.groupTimeout')
    })
    const records = Array.isArray(response) ? response : defaultResponseAdapter(response).records
    groupTreeOptions.value = normalizeTreeOptions(records)
  }
  watch(
    () => props.visible,
    async (visible) => {
      if (!visible) {
        searchForm.value = createAsnOrderMaterialSearchState()
        selectedRows.value = []
        return
      }
      searchForm.value = createAsnOrderMaterialSearchState()
      replaceSearchParams(buildAsnOrderMaterialSearchParams(searchForm.value))
      await Promise.allSettled([loadGroupTree(), getData()])
    }
  )
  watch(
    () => props.fieldDefinitions,
    () => {
      resetColumns?.()
    },
    { deep: true }
  )
</script>
rsf-design/src/views/orders/delivery-item/deliveryItemPage.helpers.js
@@ -50,21 +50,39 @@
    platItemId: '',
    matnrCode: '',
    maktx: '',
    fieldsIndex: '',
    splrName: '',
    splrCode: '',
    status: '',
    timeStart: '',
    timeEnd: '',
    memo: '',
    orderBy: 'create_time desc',
    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
      }
  ;[
    'condition',
    'deliveryCode',
    'platItemId',
    'matnrCode',
    'maktx',
    'fieldsIndex',
    '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 = normalizeNumber(params.deliveryId)
@@ -81,6 +99,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildDeliveryItemSearchParams(params)
  }
}
@@ -117,18 +136,103 @@
    batch: normalizeText(record.batch) || '--',
    trackCode: normalizeText(record.trackCode) || '--',
    packName: normalizeText(record.packName) || '--',
    prodTimeText: normalizeText(record['prodTime$'] || record.prodTimeText || record.prodTime) || '--',
    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) || '--',
    createTimeText:
      normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
    updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
    updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
    updateTimeText:
      normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
    memo: normalizeText(record.memo) || '--'
  }
}
export function createDeliveryItemFormState() {
  return {
    id: undefined,
    deliveryId: undefined,
    platItemId: '',
    matnrId: undefined,
    matnrCode: '',
    maktx: '',
    fieldsIndex: '',
    unit: '',
    anfme: undefined,
    qty: undefined,
    printQty: undefined,
    splrName: '',
    splrCode: '',
    splrBatch: '',
    status: 1,
    memo: ''
  }
}
export function buildDeliveryItemDialogModel(record = {}) {
  return {
    ...createDeliveryItemFormState(),
    ...record,
    id: record.id ?? undefined,
    deliveryId: record.deliveryId ?? undefined,
    platItemId: normalizeText(record.platItemId),
    matnrId: record.matnrId ?? undefined,
    matnrCode: normalizeText(record.matnrCode),
    maktx: normalizeText(record.maktx || record.matnrName),
    fieldsIndex: normalizeText(record.fieldsIndex),
    unit: normalizeText(record.unit),
    anfme: normalizeNumber(record.anfme, undefined),
    qty: normalizeNumber(record.qty, undefined),
    printQty: normalizeNumber(record.printQty, undefined),
    splrName: normalizeText(record.splrName),
    splrCode: normalizeText(record.splrCode),
    splrBatch: normalizeText(record.splrBatch),
    status: normalizeNumber(record.statusBool ?? record.status, 1),
    memo: normalizeText(record.memo)
  }
}
export function buildDeliveryItemSavePayload(formData = {}) {
  return {
    ...(formData.id !== undefined && formData.id !== null ? { id: Number(formData.id) } : {}),
    ...(formData.deliveryId !== undefined && formData.deliveryId !== null
      ? { deliveryId: Number(formData.deliveryId) }
      : {}),
    ...(formData.matnrId !== undefined && formData.matnrId !== null
      ? { matnrId: Number(formData.matnrId) }
      : {}),
    platItemId: normalizeText(formData.platItemId),
    matnrCode: normalizeText(formData.matnrCode),
    maktx: normalizeText(formData.maktx),
    fieldsIndex: normalizeText(formData.fieldsIndex),
    unit: normalizeText(formData.unit),
    ...(formData.anfme !== undefined && formData.anfme !== null && formData.anfme !== ''
      ? { anfme: Number(formData.anfme) }
      : {}),
    ...(formData.qty !== undefined && formData.qty !== null && formData.qty !== ''
      ? { qty: Number(formData.qty) }
      : {}),
    ...(formData.printQty !== undefined && formData.printQty !== null && formData.printQty !== ''
      ? { printQty: Number(formData.printQty) }
      : {}),
    splrName: normalizeText(formData.splrName),
    splrCode: normalizeText(formData.splrCode),
    splrBatch: normalizeText(formData.splrBatch),
    status: normalizeNumber(formData.status, 1),
    memo: normalizeText(formData.memo)
  }
}
export function getDeliveryItemStatusOptions(t = $t) {
  return [
    { value: 1, label: t('common.status.normal') },
    { value: 0, label: t('common.status.frozen') }
  ]
}
export function buildDeliveryItemPrintRows(records = [], t = $t) {
  if (!Array.isArray(records)) {
    return []
rsf-design/src/views/orders/delivery-item/deliveryItemTable.columns.js
@@ -1,12 +1,54 @@
import { h } from 'vue'
import { ElTag } from 'element-plus'
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
import { $t } from '@/locales'
export function createDeliveryItemTableColumns({ handleActionClick, t = $t } = {}) {
function createActionList({ canEdit = true, canDelete = true, t = $t } = {}) {
  const actions = [
    {
      key: 'view',
      label: t('pages.orders.delivery.actions.view'),
      icon: 'ri:eye-line'
    }
  ]
  if (canEdit) {
    actions.push({
      key: 'edit',
      label: t('common.actions.edit'),
      icon: 'ri:edit-line'
    })
  }
  if (canDelete) {
    actions.push({
      key: 'delete',
      label: t('common.actions.delete'),
      icon: 'ri:delete-bin-5-line',
      color: 'var(--art-error)'
    })
  }
  return actions
}
export function createDeliveryItemTableColumns({
  handleActionClick,
  canEdit = true,
  canDelete = true,
  t = $t
} = {}) {
  return [
    { type: 'selection', width: 48, align: 'center' },
    { type: 'globalIndex', label: t('table.index'), width: 72, align: 'center' },
    {
      prop: 'id',
      label: t('table.id'),
      width: 100,
      align: 'right',
      visible: false,
      formatter: (row) => row.id ?? '--'
    },
    {
      prop: 'deliveryCode',
      label: t('pages.orders.deliveryItem.table.deliveryCode'),
@@ -121,6 +163,38 @@
        h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '--')
    },
    {
      prop: 'updateByText',
      label: t('table.updateBy'),
      minWidth: 130,
      visible: false,
      showOverflowTooltip: true,
      formatter: (row) => row.updateByText || '--'
    },
    {
      prop: 'updateTimeText',
      label: t('table.updateTime'),
      minWidth: 170,
      visible: false,
      showOverflowTooltip: true,
      formatter: (row) => row.updateTimeText || '--'
    },
    {
      prop: 'createByText',
      label: t('table.createBy'),
      minWidth: 130,
      visible: false,
      showOverflowTooltip: true,
      formatter: (row) => row.createByText || '--'
    },
    {
      prop: 'createTimeText',
      label: t('table.createTime'),
      minWidth: 170,
      visible: false,
      showOverflowTooltip: true,
      formatter: (row) => row.createTimeText || '--'
    },
    {
      prop: 'memo',
      label: t('table.memo'),
      minWidth: 180,
@@ -130,12 +204,12 @@
    {
      prop: 'operation',
      label: t('table.operation'),
      width: 92,
      width: 120,
      fixed: 'right',
      formatter: (row) =>
        h(ArtButtonTable, {
          type: 'view',
          onClick: () => handleActionClick?.(row)
        h(ArtButtonMore, {
          list: createActionList({ canEdit, canDelete, t }),
          onClick: (item) => handleActionClick?.(item, row)
        })
    }
  ]
rsf-design/src/views/orders/delivery-item/index.vue
@@ -3,38 +3,24 @@
    <ElCard v-if="activeSourceSummary" class="mb-3">
      <div class="flex items-center justify-between gap-3">
        <div class="flex items-center gap-2 text-sm text-[var(--art-text-gray-600)]">
          <span class="font-medium text-[var(--art-text-gray-900)]">{{ t('pages.orders.deliveryItem.sourceTitle') }}</span>
          <span>{{ t('pages.orders.deliveryItem.sourceLabel', { id: activeSourceSummary.deliveryId }) }}</span>
          <span class="font-medium text-[var(--art-text-gray-900)]">{{
            t('pages.orders.deliveryItem.sourceTitle')
          }}</span>
          <span>{{
            t('pages.orders.deliveryItem.sourceLabel', { id: activeSourceSummary.deliveryId })
          }}</span>
        </div>
        <ElButton link type="primary" @click="handleClearSourceFilter">{{ t('common.actions.viewAll') }}</ElButton>
        <ElButton link type="primary" @click="handleClearSourceFilter">{{
          t('common.actions.viewAll')
        }}</ElButton>
      </div>
    </ElCard>
    <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"
    <DeliveryItemManagePanel
      :delivery-id="searchForm.deliveryId"
      :can-add="hasAuth('add')"
      :can-edit="hasAuth('update')"
      :can-delete="hasAuth('delete')"
    />
  </div>
</template>
@@ -42,164 +28,32 @@
<script setup>
  import { computed, onMounted, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import { ElButton, ElMessage } from 'element-plus'
  import { ElButton } from 'element-plus'
  import { useRoute, useRouter } from 'vue-router'
  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'
  import { useAuth } from '@/hooks/core/useAuth'
  import DeliveryItemManagePanel from './modules/delivery-item-manage-panel.vue'
  import { createDeliveryItemSearchState } from './deliveryItemPage.helpers.js'
  defineOptions({ name: 'DeliveryItem' })
  const { t } = useI18n()
  const { hasAuth } = useAuth()
  const route = useRoute()
  const router = useRouter()
  const searchForm = ref(createDeliveryItemSearchState())
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const detailData = ref({})
  const activeSourceSummary = computed(() => {
    if (searchForm.value.deliveryId === '' || searchForm.value.deliveryId === undefined || searchForm.value.deliveryId === null) {
    if (
      searchForm.value.deliveryId === '' ||
      searchForm.value.deliveryId === undefined ||
      searchForm.value.deliveryId === null
    ) {
      return null
    }
    return {
      deliveryId: searchForm.value.deliveryId
    }
  })
  const searchItems = computed(() => [
    {
      label: t('table.keyword'),
      key: 'condition',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.conditionPlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.search.deliveryCode'),
      key: 'deliveryCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.deliveryCodePlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.search.platItemId'),
      key: 'platItemId',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.platItemIdPlaceholder')
      }
    },
    {
      label: t('table.materialCode'),
      key: 'matnrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.matnrCodePlaceholder')
      }
    },
    {
      label: t('table.materialName'),
      key: 'maktx',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.maktxPlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.search.supplierName'),
      key: 'splrName',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.supplierNamePlaceholder')
      }
    },
    {
      label: t('table.supplierBatch'),
      key: 'splrBatch',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.supplierBatchPlaceholder')
      }
    }
  ])
  function openDetail(row) {
    detailDrawerVisible.value = true
    detailLoading.value = true
    guardRequestWithMessage(fetchGetDeliveryItemDetail(row.id), {}, {
      timeoutMessage: t('pages.orders.deliveryItem.messages.detailTimeout')
    })
      .then((detail) => {
        detailData.value = normalizeDeliveryItemRow(detail, t)
      })
      .catch((error) => {
        detailDrawerVisible.value = false
        detailData.value = {}
        ElMessage.error(error?.message || t('pages.orders.deliveryItem.messages.detailFailed'))
      })
      .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
      }),
      immediate: false,
      columnsFactory: () => createDeliveryItemTableColumns({ handleActionClick: openDetail, t })
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeDeliveryItemRow(item, t)) : []
    }
  })
  function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params
    }
    replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
    getData()
  }
  function handleReset() {
    Object.assign(searchForm.value, createDeliveryItemSearchState())
    resetSearchParams()
  }
  function applyRouteSearch() {
    const deliveryId = route.query.deliveryId
@@ -220,8 +74,6 @@
        deliveryId: undefined
      }
    })
    replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
    getData()
  }
  watch(
@@ -231,14 +83,10 @@
        return
      }
      applyRouteSearch()
      replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
      getData()
    }
  )
  onMounted(() => {
    applyRouteSearch()
    replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
    getData()
  })
</script>
rsf-design/src/views/orders/delivery-item/modules/delivery-item-dialog.vue
New file
@@ -0,0 +1,271 @@
<template>
  <ElDialog
    :model-value="visible"
    :title="dialogTitle"
    width="860px"
    destroy-on-close
    @update:model-value="handleVisibleChange"
    @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>
      <ElSpace>
        <ElButton @click="handleVisibleChange(false)">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" :loading="submitLoading" @click="handleSubmit">
          {{ t('common.confirm') }}
        </ElButton>
      </ElSpace>
    </template>
  </ElDialog>
</template>
<script setup>
  import { computed, nextTick, reactive, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import {
    buildDeliveryItemDialogModel,
    createDeliveryItemFormState,
    getDeliveryItemStatusOptions
  } from '../deliveryItemPage.helpers.js'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    dialogType: { type: String, default: 'add' },
    itemData: { type: Object, default: () => ({}) },
    deliveryId: { type: [Number, String], default: undefined },
    submitLoading: { type: Boolean, default: false }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const { t } = useI18n()
  const formRef = ref()
  const form = reactive(createDeliveryItemFormState())
  const isEdit = computed(() => props.dialogType === 'edit')
  const dialogTitle = computed(() =>
    isEdit.value
      ? t('pages.orders.deliveryItem.dialog.titleEdit')
      : t('pages.orders.deliveryItem.dialog.titleAdd')
  )
  const rules = computed(() => ({
    anfme: [
      {
        required: true,
        message: t('pages.orders.deliveryItem.dialog.validation.anfme'),
        trigger: 'blur'
      }
    ]
  }))
  const formItems = computed(() => [
    {
      label: t('pages.orders.deliveryItem.dialog.deliveryId'),
      key: 'deliveryId',
      type: 'input',
      hidden: true,
      props: {
        disabled: true
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.platItemId'),
      key: 'platItemId',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.platItemId')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.matnrCode'),
      key: 'matnrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.matnrCode')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.maktx'),
      key: 'maktx',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.maktx')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.fieldsIndex'),
      key: 'fieldsIndex',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.fieldsIndex')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.unit'),
      key: 'unit',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.unit')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.anfme'),
      key: 'anfme',
      type: 'number',
      props: {
        min: 0,
        precision: 2,
        controlsPosition: 'right',
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.anfme')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.qty'),
      key: 'qty',
      type: 'number',
      props: {
        min: 0,
        precision: 2,
        controlsPosition: 'right',
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.qty')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.printQty'),
      key: 'printQty',
      type: 'number',
      props: {
        min: 0,
        precision: 2,
        controlsPosition: 'right',
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.printQty')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.splrName'),
      key: 'splrName',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.splrName')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.splrCode'),
      key: 'splrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.splrCode')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.splrBatch'),
      key: 'splrBatch',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.splrBatch')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: getDeliveryItemStatusOptions(t),
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.status')
      }
    },
    {
      label: t('pages.orders.deliveryItem.dialog.memo'),
      key: 'memo',
      type: 'input',
      span: 24,
      props: {
        type: 'textarea',
        rows: 3,
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.dialog.placeholder.memo')
      }
    }
  ])
  function loadFormData() {
    const nextForm = buildDeliveryItemDialogModel({
      ...props.itemData,
      deliveryId: props.itemData?.deliveryId ?? props.deliveryId
    })
    Object.assign(form, nextForm)
  }
  function resetForm() {
    Object.assign(form, createDeliveryItemFormState(), {
      deliveryId: props.deliveryId
    })
    formRef.value?.clearValidate?.()
  }
  async function handleSubmit() {
    if (!formRef.value) {
      return
    }
    try {
      await formRef.value.validate()
      emit('submit', { ...form })
    } catch {
      return
    }
  }
  function handleVisibleChange(value) {
    emit('update:visible', value)
  }
  function handleClosed() {
    resetForm()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (!visible) {
        return
      }
      loadFormData()
      nextTick(() => {
        formRef.value?.clearValidate?.()
      })
    },
    { immediate: true }
  )
  watch(
    () => props.itemData,
    () => {
      if (props.visible) {
        loadFormData()
      }
    },
    { deep: true }
  )
</script>
rsf-design/src/views/orders/delivery-item/modules/delivery-item-manage-panel.vue
New file
@@ -0,0 +1,441 @@
<template>
  <div class="flex flex-col gap-4">
    <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="!canCreateItem" @click="openCreateDialog">
              {{ t('pages.orders.deliveryItem.actions.add') }}
            </ElButton>
            <ElButton
              type="danger"
              plain
              :disabled="!selectedRows.length || !canDelete"
              @click="handleBatchDelete"
            >
              {{ t('common.actions.batchDelete') }}
            </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>
    <DeliveryItemDetailDrawer
      v-model:visible="detailDrawerVisible"
      :loading="detailLoading"
      :detail="detailData"
    />
    <DeliveryItemDialog
      v-model:visible="dialogVisible"
      :dialog-type="dialogType"
      :item-data="currentItemData"
      :delivery-id="resolvedDeliveryId"
      :submit-loading="submitLoading"
      @submit="handleDialogSubmit"
    />
  </div>
</template>
<script setup>
  import { computed, ref, watch } from 'vue'
  import { ElMessage, ElMessageBox } from 'element-plus'
  import { useI18n } from 'vue-i18n'
  import { useTable } from '@/hooks/core/useTable'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import {
    fetchDeleteDeliveryItem,
    fetchDeleteDeliveryItemMany,
    fetchDeliveryItemPage,
    fetchGetDeliveryItemDetail,
    fetchSaveDeliveryItem,
    fetchUpdateDeliveryItem
  } from '@/api/delivery'
  import DeliveryItemDetailDrawer from './delivery-item-detail-drawer.vue'
  import DeliveryItemDialog from './delivery-item-dialog.vue'
  import { createDeliveryItemTableColumns } from '../deliveryItemTable.columns.js'
  import {
    buildDeliveryItemDialogModel,
    buildDeliveryItemPageQueryParams,
    buildDeliveryItemSavePayload,
    createDeliveryItemFormState,
    createDeliveryItemSearchState,
    getDeliveryItemStatusOptions,
    normalizeDeliveryItemRow
  } from '../deliveryItemPage.helpers.js'
  const props = defineProps({
    deliveryId: { type: [Number, String], default: undefined },
    canEdit: { type: Boolean, default: true },
    canDelete: { type: Boolean, default: true },
    canAdd: { type: Boolean, default: true }
  })
  const emit = defineEmits(['changed'])
  const { t } = useI18n()
  const searchForm = ref(createDeliveryItemSearchState())
  const selectedRows = ref([])
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const detailData = ref({})
  const dialogVisible = ref(false)
  const dialogType = ref('add')
  const currentItemData = ref(createDeliveryItemFormState())
  const submitLoading = ref(false)
  const resolvedDeliveryId = computed(() => {
    if (props.deliveryId === '' || props.deliveryId === undefined || props.deliveryId === null) {
      return undefined
    }
    const numericValue = Number(props.deliveryId)
    return Number.isFinite(numericValue) ? numericValue : undefined
  })
  const canCreateItem = computed(() => props.canAdd && resolvedDeliveryId.value !== undefined)
  const searchItems = computed(() => [
    {
      label: t('table.keyword'),
      key: 'condition',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.conditionPlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.search.deliveryCode'),
      key: 'deliveryCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.deliveryCodePlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.search.platItemId'),
      key: 'platItemId',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.platItemIdPlaceholder')
      }
    },
    {
      label: t('table.materialCode'),
      key: 'matnrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.matnrCodePlaceholder')
      }
    },
    {
      label: t('table.materialName'),
      key: 'maktx',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.maktxPlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.table.fieldsIndex'),
      key: 'fieldsIndex',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.fieldsIndexPlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.search.supplierName'),
      key: 'splrName',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.supplierNamePlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.table.supplierCode'),
      key: 'splrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.supplierCodePlaceholder')
      }
    },
    {
      label: t('table.supplierBatch'),
      key: 'splrBatch',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.supplierBatchPlaceholder')
      }
    },
    {
      label: t('table.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: getDeliveryItemStatusOptions(t),
        placeholder: t('pages.orders.deliveryItem.search.statusPlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.search.timeStart'),
      key: 'timeStart',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        placeholder: t('pages.orders.deliveryItem.search.timeStartPlaceholder')
      }
    },
    {
      label: t('pages.orders.deliveryItem.search.timeEnd'),
      key: 'timeEnd',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        placeholder: t('pages.orders.deliveryItem.search.timeEndPlaceholder')
      }
    },
    {
      label: t('table.memo'),
      key: 'memo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.deliveryItem.search.memoPlaceholder')
      }
    }
  ])
  const {
    columns,
    columnChecks,
    data,
    loading,
    pagination,
    replaceSearchParams,
    handleSizeChange,
    handleCurrentChange,
    refreshData,
    getData
  } = useTable({
    core: {
      apiFn: fetchDeliveryItemPage,
      apiParams: buildDeliveryItemPageQueryParams(searchForm.value),
      immediate: false,
      columnsFactory: () =>
        createDeliveryItemTableColumns({
          handleActionClick: handleActionClick,
          canEdit: props.canEdit,
          canDelete: props.canDelete,
          t
        })
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeDeliveryItemRow(item, t)) : []
    }
  })
  function syncDeliveryFilter() {
    searchForm.value.deliveryId = resolvedDeliveryId.value ?? ''
  }
  function handleSelectionChange(rows) {
    selectedRows.value = Array.isArray(rows) ? rows : []
  }
  function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params,
      deliveryId: resolvedDeliveryId.value ?? searchForm.value.deliveryId
    }
    replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
    getData()
  }
  function handleReset() {
    searchForm.value = createDeliveryItemSearchState()
    syncDeliveryFilter()
    replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
    getData()
  }
  async function openDetail(row) {
    detailDrawerVisible.value = true
    detailLoading.value = true
    try {
      const detail = await guardRequestWithMessage(
        fetchGetDeliveryItemDetail(row.id),
        {},
        {
          timeoutMessage: t('pages.orders.deliveryItem.messages.detailTimeout')
        }
      )
      detailData.value = normalizeDeliveryItemRow(detail, t)
    } catch (error) {
      detailDrawerVisible.value = false
      detailData.value = {}
      ElMessage.error(error?.message || t('pages.orders.deliveryItem.messages.detailFailed'))
    } finally {
      detailLoading.value = false
    }
  }
  async function openEditDialog(row) {
    dialogType.value = 'edit'
    currentItemData.value = buildDeliveryItemDialogModel(row)
    dialogVisible.value = true
  }
  function openCreateDialog() {
    if (!canCreateItem.value) {
      return
    }
    dialogType.value = 'add'
    currentItemData.value = buildDeliveryItemDialogModel({
      deliveryId: resolvedDeliveryId.value
    })
    dialogVisible.value = true
  }
  async function handleDelete(row) {
    await ElMessageBox.confirm(
      t('pages.orders.deliveryItem.messages.deleteConfirm', {
        code: row.platItemId || row.id || ''
      }),
      t('crud.confirm.deleteTitle'),
      {
        confirmButtonText: t('common.confirm'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      }
    )
    await fetchDeleteDeliveryItem(row.id)
    ElMessage.success(t('crud.messages.deleteSuccess'))
    await refreshData()
    emit('changed')
  }
  async function handleBatchDelete() {
    if (!selectedRows.value.length) {
      return
    }
    await ElMessageBox.confirm(
      t('crud.confirm.batchDeleteMessage', {
        count: selectedRows.value.length,
        entity: t('menu.deliveryItem')
      }),
      t('crud.confirm.batchDeleteTitle'),
      {
        confirmButtonText: t('common.confirm'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      }
    )
    await fetchDeleteDeliveryItemMany(selectedRows.value.map((item) => item.id))
    ElMessage.success(t('crud.messages.batchDeleteSuccess'))
    selectedRows.value = []
    await refreshData()
    emit('changed')
  }
  async function handleActionClick(action, row) {
    try {
      if (action.key === 'view') {
        await openDetail(row)
        return
      }
      if (action.key === 'edit') {
        await openEditDialog(row)
        return
      }
      if (action.key === 'delete') {
        await handleDelete(row)
      }
    } catch (error) {
      if (error === 'cancel' || error?.message === 'cancel') {
        return
      }
      ElMessage.error(error?.message || t('pages.orders.deliveryItem.messages.actionFailed'))
    }
  }
  async function handleDialogSubmit(payload) {
    submitLoading.value = true
    try {
      const requestPayload = buildDeliveryItemSavePayload({
        ...payload,
        deliveryId: payload.deliveryId ?? resolvedDeliveryId.value
      })
      if (dialogType.value === 'edit') {
        await fetchUpdateDeliveryItem(requestPayload)
        ElMessage.success(t('pages.orders.deliveryItem.messages.updateSuccess'))
      } else {
        await fetchSaveDeliveryItem(requestPayload)
        ElMessage.success(t('pages.orders.deliveryItem.messages.createSuccess'))
      }
      dialogVisible.value = false
      await refreshData()
      emit('changed')
    } catch (error) {
      ElMessage.error(
        error?.message ||
          (dialogType.value === 'edit'
            ? t('pages.orders.deliveryItem.messages.updateFailed')
            : t('pages.orders.deliveryItem.messages.createFailed'))
      )
    } finally {
      submitLoading.value = false
    }
  }
  async function reload() {
    syncDeliveryFilter()
    replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
    await getData()
  }
  watch(
    () => props.deliveryId,
    () => {
      syncDeliveryFilter()
      replaceSearchParams(buildDeliveryItemPageQueryParams(searchForm.value))
      getData()
    },
    { immediate: true }
  )
  defineExpose({
    reload
  })
</script>
rsf-design/src/views/orders/delivery/deliveryPage.helpers.js
@@ -60,23 +60,51 @@
    }
    return fallback
  }
  return deliveryExceStatusMeta[Number(exceStatus)] || {
    text: normalizeText(exceStatus) || '--',
    type: 'info'
  }
  return (
    deliveryExceStatusMeta[Number(exceStatus)] || {
      text: normalizeText(exceStatus) || '--',
      type: 'info'
    }
  )
}
export function createDeliverySearchState() {
  return {
    condition: '',
    timeStart: '',
    timeEnd: '',
    code: '',
    platId: '',
    type: '',
    wkType: '',
    source: '',
    anfme: '',
    qty: '',
    workQty: '',
    platCode: '',
    startTime: '',
    endTime: '',
    status: '',
    exceStatus: '',
    memo: ''
    memo: '',
    orderBy: 'create_time desc'
  }
}
export function getDeliveryStatusOptions(t = $t) {
  return [
    { value: 1, label: t('pages.orders.delivery.status.normal') },
    { value: 0, label: t('pages.orders.delivery.status.disabled') }
  ]
}
export function getDeliveryExceStatusOptions(t = $t) {
  return [
    { value: 0, label: t('pages.orders.delivery.status.pending') },
    { value: 1, label: t('pages.orders.delivery.status.running') },
    { value: 2, label: t('pages.orders.delivery.status.partial') },
    { value: 3, label: t('pages.orders.delivery.status.completed') }
  ]
}
export function getDeliveryPaginationKey() {
@@ -88,10 +116,17 @@
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
  ;['condition', 'code', 'platId', 'type', 'wkType', 'source', 'platCode', 'memo'].forEach(
    (key) => {
      const value = normalizeText(params[key])
      if (value) {
        result[key] = value
      }
    }
  )
  ;['anfme', 'qty', 'workQty'].forEach((key) => {
    if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
      result[key] = normalizeNumber(params[key])
    }
  })
@@ -111,6 +146,14 @@
    result.timeEnd = normalizeText(params.timeEnd)
  }
  if (params.startTime !== '' && params.startTime !== undefined && params.startTime !== null) {
    result.startTime = normalizeText(params.startTime)
  }
  if (params.endTime !== '' && params.endTime !== undefined && params.endTime !== null) {
    result.endTime = normalizeText(params.endTime)
  }
  return result
}
@@ -118,6 +161,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildDeliverySearchParams(params)
  }
}
@@ -132,7 +176,11 @@
export function normalizeDeliveryRow(record = {}, t = $t) {
  const statusMeta = normalizeStatusMeta(record.statusBool ?? record.status, t)
  const exceStatusMeta = normalizeExceStatusMeta(record.exceStatus, record['exceStatus$'] || record.exceStatusText, t)
  const exceStatusMeta = normalizeExceStatusMeta(
    record.exceStatus,
    record['exceStatus$'] || record.exceStatusText,
    t
  )
  return {
    ...record,
    id: record.id ?? null,
@@ -145,17 +193,21 @@
    statusText: statusMeta.text,
    statusType: statusMeta.type,
    statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
    exceStatusText: normalizeText(record['exceStatus$'] || record.exceStatusText) || exceStatusMeta.text,
    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) || '--',
    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) || '--',
    createTimeText:
      normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
    updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
    updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
    updateTimeText:
      normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
    memo: normalizeText(record.memo) || '--'
  }
}
@@ -187,17 +239,17 @@
    batch: normalizeText(record.batch) || '--',
    trackCode: normalizeText(record.trackCode) || '--',
    packName: normalizeText(record.packName) || '--',
    prodTimeText: normalizeText(record['prodTime$'] || record.prodTimeText || record.prodTime) || '--',
    prodTimeText:
      normalizeText(record['prodTime$'] || record.prodTimeText || record.prodTime) || '--',
    statusText: statusMeta.text,
    statusType: statusMeta.type,
    statusBool:
      record.statusBool !== void 0
        ? Boolean(record.statusBool)
        : statusMeta.bool,
    statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
    createByText: normalizeText(record['createBy$'] || record.createByText) || '--',
    createTimeText: normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
    createTimeText:
      normalizeText(record['createTime$'] || record.createTimeText || record.createTime) || '--',
    updateByText: normalizeText(record['updateBy$'] || record.updateByText) || '--',
    updateTimeText: normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
    updateTimeText:
      normalizeText(record['updateTime$'] || record.updateTimeText || record.updateTime) || '--',
    memo: normalizeText(record.memo) || '--'
  }
}
@@ -254,22 +306,32 @@
  }
}
export function getDeliveryActionList(row = {}) {
export function getDeliveryActionList(row = {}, options = {}) {
  const normalizedRow = normalizeDeliveryRow(row)
  const { canEdit = true, canDelete = true } = options
  const actions = [
    {
      key: 'view',
      label: $t('pages.orders.delivery.actions.view'),
      icon: 'ri:eye-line'
    },
    {
      key: 'items',
      label: $t('pages.orders.delivery.actions.items'),
      icon: 'ri:list-check-3'
    }
  ]
  if (Number(normalizedRow.exceStatus) === 0) {
  if (canEdit) {
    actions.push({
      key: 'edit',
      label: $t('pages.orders.delivery.actions.edit'),
      icon: 'ri:edit-line'
    })
  }
  actions.push({
    key: 'items',
    label: $t('pages.orders.delivery.actions.items'),
    icon: 'ri:list-check-3'
  })
  if (canDelete && Number(normalizedRow.exceStatus) === 0) {
    actions.push({
      key: 'delete',
      label: $t('pages.orders.delivery.actions.delete'),
rsf-design/src/views/orders/delivery/deliveryTable.columns.js
@@ -4,10 +4,22 @@
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
import { getDeliveryActionList } from './deliveryPage.helpers.js'
export function createDeliveryTableColumns({ handleActionClick } = {}) {
export function createDeliveryTableColumns({
  handleActionClick,
  canEdit = true,
  canDelete = true
} = {}) {
  return [
    { type: 'selection', width: 48, align: 'center' },
    { type: 'globalIndex', label: $t('table.index'), width: 72, align: 'center' },
    {
      prop: 'id',
      label: $t('table.id'),
      width: 100,
      align: 'right',
      visible: false,
      formatter: (row) => row.id ?? '--'
    },
    {
      prop: 'code',
      label: $t('pages.orders.delivery.search.code'),
@@ -73,7 +85,7 @@
    },
    {
      prop: 'status',
      label: $t('pages.orders.transfer.search.status'),
      label: $t('table.status'),
      width: 96,
      align: 'center',
      formatter: (row) =>
@@ -85,7 +97,11 @@
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) =>
        h(ElTag, { type: row.exceStatusTagType || 'info', effect: 'light' }, () => row.exceStatusText || '--')
        h(
          ElTag,
          { type: row.exceStatusTagType || 'info', effect: 'light' },
          () => row.exceStatusText || '--'
        )
    },
    {
      prop: 'startTimeText',
@@ -102,6 +118,38 @@
      formatter: (row) => row.endTimeText || '--'
    },
    {
      prop: 'updateByText',
      label: $t('table.updateBy'),
      minWidth: 130,
      visible: false,
      showOverflowTooltip: true,
      formatter: (row) => row.updateByText || '--'
    },
    {
      prop: 'updateTimeText',
      label: $t('table.updateTime'),
      minWidth: 170,
      visible: false,
      showOverflowTooltip: true,
      formatter: (row) => row.updateTimeText || '--'
    },
    {
      prop: 'createByText',
      label: $t('table.createBy'),
      minWidth: 130,
      visible: false,
      showOverflowTooltip: true,
      formatter: (row) => row.createByText || '--'
    },
    {
      prop: 'createTimeText',
      label: $t('table.createTime'),
      minWidth: 170,
      visible: false,
      showOverflowTooltip: true,
      formatter: (row) => row.createTimeText || '--'
    },
    {
      prop: 'memo',
      label: $t('pages.orders.delivery.search.memo'),
      minWidth: 180,
@@ -116,7 +164,7 @@
      fixed: 'right',
      formatter: (row) =>
        h(ArtButtonMore, {
          list: getDeliveryActionList(row),
          list: getDeliveryActionList(row, { canEdit, canDelete }),
          onClick: (item) => handleActionClick?.(item, row)
        })
    }
rsf-design/src/views/orders/delivery/index.vue
@@ -11,21 +11,50 @@
    <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"
          />
          <ElSpace wrap>
            <ElUpload
              v-auth="'update'"
              :auto-upload="false"
              :show-file-list="false"
              accept=".xlsx,.xls"
              @change="handleImportFileChange"
            >
              <ElButton :loading="importing">{{
                t('pages.orders.delivery.buttons.import')
              }}</ElButton>
            </ElUpload>
            <ElButton
              v-auth="'update'"
              :loading="templateDownloading"
              @click="handleDownloadTemplate"
            >
              {{ t('pages.orders.delivery.buttons.downloadTemplate') }}
            </ElButton>
            <ElButton
              v-auth="'delete'"
              type="danger"
              plain
              :disabled="selectedRows.length === 0"
              @click="handleBatchDelete"
            >
              {{ t('common.actions.batchDelete') }}
            </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>
@@ -50,15 +79,24 @@
      @size-change="handleDetailSizeChange"
      @current-change="handleDetailCurrentChange"
    />
    <DeliveryManageDialog
      v-model:visible="manageDialogVisible"
      :delivery="manageDeliveryData"
      :can-add="hasAuth('update')"
      :can-edit="hasAuth('update')"
      :can-delete="hasAuth('delete')"
      @changed="handleManageChanged"
    />
  </div>
</template>
<script setup>
  import { computed, reactive, ref } from 'vue'
  import { useRouter } from 'vue-router'
  import { useI18n } from 'vue-i18n'
  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'
@@ -73,24 +111,29 @@
    createDeliverySearchState,
    getDeliveryReportTitle,
    getDeliveryPaginationKey,
    getDeliveryStatusOptions,
    getDeliveryExceStatusOptions,
    normalizeDeliveryItemRow,
    normalizeDeliveryRow
  } from './deliveryPage.helpers.js'
  import {
    fetchDeleteDelivery,
    fetchDeleteDeliveryMany,
    fetchDeliveryItemPage,
    fetchDeliveryPage,
    fetchDownloadDeliveryTemplate,
    fetchExportDeliveryReport,
    fetchGetDeliveryDetail,
    fetchGetDeliveryMany
  } from '@/api/delivery'
  import { fetchImportDelivery } from '@/api/delivery'
  import DeliveryDetailDrawer from './modules/delivery-detail-drawer.vue'
  import DeliveryManageDialog from './modules/delivery-manage-dialog.vue'
  import { createDeliveryTableColumns } from './deliveryTable.columns.js'
  defineOptions({ name: 'Delivery' })
  const userStore = useUserStore()
  const router = useRouter()
  const { hasAuth } = useAuth()
  const { t } = useI18n()
  const reportTitle = computed(() => getDeliveryReportTitle(t))
  const searchForm = ref(createDeliverySearchState())
@@ -101,6 +144,10 @@
  const detailData = ref({})
  const detailItemRows = ref([])
  const activeDeliveryId = ref(null)
  const importing = ref(false)
  const templateDownloading = ref(false)
  const manageDialogVisible = ref(false)
  const manageDeliveryData = ref({})
  const detailItemPagination = reactive({
    current: 1,
    size: 20,
@@ -116,6 +163,26 @@
      props: {
        clearable: true,
        placeholder: t('pages.orders.delivery.placeholder.condition')
      }
    },
    {
      label: t('pages.orders.delivery.search.timeStart'),
      key: 'timeStart',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        placeholder: t('pages.orders.delivery.placeholder.timeStart')
      }
    },
    {
      label: t('pages.orders.delivery.search.timeEnd'),
      key: 'timeEnd',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        placeholder: t('pages.orders.delivery.placeholder.timeEnd')
      }
    },
    {
@@ -164,11 +231,84 @@
      }
    },
    {
      label: t('pages.orders.delivery.search.exceStatus'),
      key: 'exceStatus',
      label: t('pages.orders.delivery.search.anfme'),
      key: 'anfme',
      type: 'number',
      props: {
        min: 0,
        precision: 2,
        controlsPosition: 'right',
        placeholder: t('pages.orders.delivery.placeholder.anfme')
      }
    },
    {
      label: t('pages.orders.delivery.search.qty'),
      key: 'qty',
      type: 'number',
      props: {
        min: 0,
        precision: 2,
        controlsPosition: 'right',
        placeholder: t('pages.orders.delivery.placeholder.qty')
      }
    },
    {
      label: t('pages.orders.delivery.search.workQty'),
      key: 'workQty',
      type: 'number',
      props: {
        min: 0,
        precision: 2,
        controlsPosition: 'right',
        placeholder: t('pages.orders.delivery.placeholder.workQty')
      }
    },
    {
      label: t('pages.orders.delivery.search.platCode'),
      key: 'platCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.delivery.placeholder.platCode')
      }
    },
    {
      label: t('pages.orders.delivery.search.startTime'),
      key: 'startTime',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        placeholder: t('pages.orders.delivery.placeholder.startTime')
      }
    },
    {
      label: t('pages.orders.delivery.search.endTime'),
      key: 'endTime',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        placeholder: t('pages.orders.delivery.placeholder.endTime')
      }
    },
    {
      label: t('table.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: getDeliveryStatusOptions(t),
        placeholder: t('pages.orders.delivery.placeholder.status')
      }
    },
    {
      label: t('pages.orders.delivery.search.exceStatus'),
      key: 'exceStatus',
      type: 'select',
      props: {
        clearable: true,
        options: getDeliveryExceStatusOptions(t),
        placeholder: t('pages.orders.delivery.placeholder.exceStatus')
      }
    },
@@ -208,9 +348,13 @@
        { timeoutMessage: t('pages.orders.delivery.messages.itemsTimeout') }
      )
      const normalizedResponse = defaultResponseAdapter(response)
      detailItemRows.value = normalizedResponse.records.map((item) => normalizeDeliveryItemRow(item, t))
      detailItemRows.value = normalizedResponse.records.map((item) =>
        normalizeDeliveryItemRow(item, t)
      )
      detailItemPagination.total = Number(normalizedResponse.total || 0)
      detailItemPagination.current = Number(normalizedResponse.current || detailItemPagination.current || 1)
      detailItemPagination.current = Number(
        normalizedResponse.current || detailItemPagination.current || 1
      )
      detailItemPagination.size = Number(normalizedResponse.size || detailItemPagination.size || 20)
    } finally {
      detailItemsLoading.value = false
@@ -248,51 +392,75 @@
  }
  async function handleDelete(row) {
    try {
      await ElMessageBox.confirm(
        t('crud.confirm.deleteMessage', {
          entity: t('pages.orders.delivery.entity'),
          label: row.code || row.id
        }),
        t('crud.confirm.deleteTitle'),
        {
          confirmButtonText: t('common.confirm'),
          cancelButtonText: t('common.cancel'),
          type: 'warning'
        }
      )
      await fetchDeleteDelivery(row.id)
      ElMessage.success(t('crud.messages.deleteSuccess'))
      await refreshRemove()
    } catch (error) {
      if (error !== 'cancel') {
        ElMessage.error(error?.message || t('crud.messages.deleteFailed'))
    await ElMessageBox.confirm(
      t('crud.confirm.deleteMessage', {
        entity: t('pages.orders.delivery.entity'),
        label: row.code || row.id
      }),
      t('crud.confirm.deleteTitle'),
      {
        confirmButtonText: t('common.confirm'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      }
    }
    )
    await fetchDeleteDeliveryMany([row.id])
    ElMessage.success(t('crud.messages.deleteSuccess'))
    await refreshRemove()
  }
  function handleTableActionClick(action, row) {
  function openManageDialog(row) {
    manageDeliveryData.value = normalizeDeliveryRow(row, t)
    manageDialogVisible.value = true
  }
  async function handleBatchDelete() {
    if (!selectedRows.value.length) {
      return
    }
    await ElMessageBox.confirm(
      t('crud.confirm.batchDeleteMessage', {
        count: selectedRows.value.length,
        entity: t('pages.orders.delivery.entity')
      }),
      t('crud.confirm.batchDeleteTitle'),
      {
        confirmButtonText: t('common.confirm'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      }
    )
    await fetchDeleteDeliveryMany(selectedRows.value.map((item) => item.id))
    ElMessage.success(t('crud.messages.batchDeleteSuccess'))
    selectedRows.value = []
    await refreshData()
  }
  async function handleTableActionClick(action, row) {
    if (!action) {
      return
    }
    if (action.key === 'view') {
      openDetail(row)
      await openDetail(row)
      return
    }
    if (action.key === 'edit') {
      openManageDialog(row)
      return
    }
    if (action.key === 'items') {
      if (!row?.id) {
        return
      }
      router.push({
        path: '/orders/delivery-item',
        query: {
          deliveryId: String(row.id)
        }
      })
      openManageDialog(row)
      return
    }
    if (action.key === 'delete') {
      handleDelete(row)
      try {
        await handleDelete(row)
      } catch (error) {
        if (error === 'cancel' || error?.message === 'cancel') {
          return
        }
        ElMessage.error(error?.message || t('crud.messages.deleteFailed'))
      }
    }
  }
@@ -317,10 +485,16 @@
        pageSize: 20
      }),
      paginationKey: getDeliveryPaginationKey(),
      columnsFactory: () => createDeliveryTableColumns({ handleActionClick: handleTableActionClick })
      columnsFactory: () =>
        createDeliveryTableColumns({
          handleActionClick: handleTableActionClick,
          canEdit: hasAuth('update'),
          canDelete: hasAuth('delete')
        })
    },
    transform: {
      dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeDeliveryRow(item, t)) : [])
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeDeliveryRow(item, t)) : []
    }
  })
@@ -338,6 +512,69 @@
    resetSearchParams()
  }
  async function handleManageChanged() {
    await refreshData()
    if (detailDrawerVisible.value && activeDeliveryId.value) {
      await loadDetailItems(activeDeliveryId.value)
    }
  }
  async function handleImportFileChange(uploadFile) {
    if (!uploadFile?.raw) {
      return
    }
    importing.value = true
    try {
      await fetchImportDelivery(uploadFile.raw)
      ElMessage.success(t('pages.orders.delivery.messages.importSuccess'))
      await refreshData()
    } catch (error) {
      ElMessage.error(error?.message || t('pages.orders.delivery.messages.importFailed'))
    } finally {
      importing.value = false
    }
  }
  async function downloadFile(response, fallbackName) {
    const blob = await response.blob()
    if (!blob || !blob.size) {
      throw new Error(t('pages.orders.delivery.messages.templateDownloadFailed'))
    }
    const disposition = response.headers.get('Content-Disposition') || ''
    const matchedName =
      disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1] ||
      disposition.match(/filename="?([^";]+)"?/i)?.[1]
    const fileName = matchedName ? decodeURIComponent(matchedName) : fallbackName
    const url = URL.createObjectURL(blob)
    const anchor = document.createElement('a')
    anchor.href = url
    anchor.download = fileName
    document.body.appendChild(anchor)
    anchor.click()
    anchor.remove()
    URL.revokeObjectURL(url)
  }
  async function handleDownloadTemplate() {
    templateDownloading.value = true
    try {
      const response = await fetchDownloadDeliveryTemplate(
        {},
        {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }
      )
      await downloadFile(response, 'delivery-template.xlsx')
      ElMessage.success(t('pages.orders.delivery.messages.templateDownloadSuccess'))
    } catch (error) {
      ElMessage.error(error?.message || t('pages.orders.delivery.messages.templateDownloadFailed'))
    } finally {
      templateDownloading.value = false
    }
  }
  const resolvePrintRecords = async (payload) => {
    if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
      return defaultResponseAdapter(await fetchGetDeliveryMany(payload.ids)).records
@@ -346,7 +583,8 @@
      await fetchDeliveryPage({
        ...reportQueryParams.value,
        current: 1,
        pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
        pageSize:
          Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
      })
    ).records
  }
rsf-design/src/views/orders/delivery/modules/delivery-manage-dialog.vue
New file
@@ -0,0 +1,86 @@
<template>
  <ElDialog
    :model-value="visible"
    :title="t('pages.orders.delivery.manage.title')"
    width="94%"
    top="4vh"
    destroy-on-close
    @update:model-value="handleVisibleChange"
  >
    <div class="flex flex-col gap-4">
      <ElDescriptions
        :title="t('pages.orders.delivery.manage.baseInfo')"
        :column="3"
        border
        size="small"
      >
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.code')">{{
          delivery.code || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.platId')">{{
          delivery.platId || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.platCode')">{{
          delivery.platCode || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.type')">{{
          delivery.typeLabel || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.wkType')">{{
          delivery.wkTypeLabel || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.source')">{{
          delivery.source || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.anfme')">{{
          delivery.anfme ?? '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.qty')">{{
          delivery.qty ?? '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.workQty')">{{
          delivery.workQty ?? '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.startTime')">{{
          delivery.startTimeText || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.endTime')">{{
          delivery.endTimeText || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem :label="t('pages.orders.delivery.detail.memo')">{{
          delivery.memo || '--'
        }}</ElDescriptionsItem>
      </ElDescriptions>
      <DeliveryItemManagePanel
        :delivery-id="delivery.id"
        :can-add="canAdd"
        :can-edit="canEdit"
        :can-delete="canDelete"
        @changed="emit('changed')"
      />
    </div>
  </ElDialog>
</template>
<script setup>
  import { useI18n } from 'vue-i18n'
  import DeliveryItemManagePanel from '@/views/orders/delivery-item/modules/delivery-item-manage-panel.vue'
  defineOptions({ name: 'DeliveryManageDialog' })
  defineProps({
    visible: { type: Boolean, default: false },
    delivery: { type: Object, default: () => ({}) },
    canAdd: { type: Boolean, default: true },
    canEdit: { type: Boolean, default: true },
    canDelete: { type: Boolean, default: true }
  })
  const emit = defineEmits(['update:visible', 'changed'])
  const { t } = useI18n()
  function handleVisibleChange(value) {
    emit('update:visible', value)
  }
</script>
rsf-design/src/views/orders/preparation-item/index.vue
@@ -21,21 +21,34 @@
    <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"
          />
          <ElSpace wrap>
            <ElButton type="primary" :disabled="!canCreateItem" @click="openCreateDialog">
              新增明细
            </ElButton>
            <ElButton
              type="danger"
              plain
              :disabled="!selectedRows.length || !canDelete"
              @click="handleBatchDelete"
            >
              批量删除
            </ElButton>
            <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"
            />
          </ElSpace>
        </template>
      </ArtTableHeader>
@@ -55,13 +68,23 @@
      :loading="detailLoading"
      :detail="detailData"
    />
    <PreparationItemDialog
      v-model:visible="dialogVisible"
      :dialog-type="dialogType"
      :item-data="currentItemData"
      :order-id="resolvedOrderId"
      :submit-loading="submitLoading"
      @submit="handleDialogSubmit"
    />
  </div>
</template>
<script setup>
  import { computed, onMounted, ref, watch } from 'vue'
  import { useRoute, useRouter } from 'vue-router'
  import { ElButton, ElMessage } from 'element-plus'
  import { ElButton, ElMessage, ElMessageBox, ElSpace } from 'element-plus'
  import { useAuth } from '@/hooks/core/useAuth'
  import { useUserStore } from '@/store/modules/user'
  import { useTable } from '@/hooks/core/useTable'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
@@ -69,20 +92,27 @@
  import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
  import ListExportPrint from '@/components/biz/list-export-print/index.vue'
  import {
    fetchDeletePreparationItem,
    fetchExportPreparationItemReport,
    fetchGetPreparationItemDetail,
    fetchGetPreparationItemMany,
    fetchPreparationItemPage
    fetchPreparationItemPage,
    fetchSavePreparationItem,
    fetchUpdatePreparationItem
  } 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 PreparationItemDialog from './modules/preparation-item-dialog.vue'
  import { createPreparationItemTableColumns } from './preparationItemTable.columns.js'
  import {
    PREPARATION_ITEM_REPORT_STYLE,
    PREPARATION_ITEM_REPORT_TITLE,
    buildPreparationItemDialogModel,
    buildPreparationItemPageQueryParams,
    buildPreparationItemPrintRows,
    buildPreparationItemReportMeta,
    buildPreparationItemSavePayload,
    buildPreparationItemSearchParams,
    createPreparationItemFormState,
    createPreparationItemSearchState,
    getPreparationItemPaginationKey,
    getPreparationItemReportColumns,
@@ -93,6 +123,7 @@
  const route = useRoute()
  const router = useRouter()
  const { hasAuth } = useAuth()
  const userStore = useUserStore()
  const initialOrderId = route.query.orderId || route.query.id
  const searchForm = ref(
@@ -104,126 +135,163 @@
  const detailLoading = ref(false)
  const detailData = ref({})
  const selectedRows = ref([])
  const dialogVisible = ref(false)
  const dialogType = ref('add')
  const currentItemData = ref(createPreparationItemFormState())
  const submitLoading = ref(false)
  const reportTitle = PREPARATION_ITEM_REPORT_TITLE
  const reportColumns = getPreparationItemReportColumns()
  const reportQueryParams = computed(() => buildPreparationItemSearchParams(searchForm.value))
  const activeSourceSummary = computed(() => {
  const resolvedOrderId = computed(() => {
    if (
      searchForm.value.orderId === '' ||
      searchForm.value.orderId === undefined ||
      searchForm.value.orderId === null
    ) {
      return null
      return undefined
    }
    return {
      orderId: searchForm.value.orderId
    }
    const numericValue = Number(searchForm.value.orderId)
    return Number.isFinite(numericValue) ? numericValue : undefined
  })
  const canUpdate = computed(() => hasAuth('update'))
  const canDelete = computed(() => hasAuth('delete'))
  const canCreateItem = computed(() => hasAuth('add') && resolvedOrderId.value !== undefined)
  const activeSourceSummary = computed(() =>
    resolvedOrderId.value === undefined
      ? null
      : {
          orderId: resolvedOrderId.value
        }
  )
  const searchItems = computed(() => [
    {
      label: '关键字',
      key: 'condition',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入备料单号/物料编码/物料名称'
      }
      props: { clearable: true, placeholder: '请输入备料单号/物料编码/物料名称' }
    },
    {
      label: '备料单ID',
      key: 'orderId',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: '请输入备料单ID'
      }
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入备料单ID' }
    },
    {
      label: '备料单号',
      key: 'orderCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入备料单号'
      }
      props: { clearable: true, placeholder: '请输入备料单号' }
    },
    {
      label: 'PO单号',
      key: 'poCode',
      label: 'PO明细ID',
      key: 'poDetlId',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入PO单号'
      }
      props: { clearable: true, placeholder: '请输入 PO 明细ID' }
    },
    {
      label: '物料编码',
      key: 'matnrCode',
      label: '物料ID',
      key: 'matnrId',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入物料编码'
      }
      props: { clearable: true, placeholder: '请输入物料ID' }
    },
    {
      label: '物料名称',
      key: 'maktx',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入物料名称'
      }
      props: { clearable: true, placeholder: '请输入物料名称' }
    },
    {
      label: '物料编码',
      key: 'matnrCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入物料编码' }
    },
    {
      label: '计划数量',
      key: 'anfme',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入计划数量' }
    },
    {
      label: '库存单位',
      key: 'stockUnit',
      type: 'input',
      props: { clearable: true, placeholder: '请输入库存单位' }
    },
    {
      label: '采购数量',
      key: 'purQty',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入采购数量' }
    },
    {
      label: '采购单位',
      key: 'purUnit',
      type: 'input',
      props: { clearable: true, placeholder: '请输入采购单位' }
    },
    {
      label: '已出数量',
      key: 'qty',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入已出数量' }
    },
    {
      label: '供应商编码',
      key: 'splrCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入供应商编码' }
    },
    {
      label: '供应商',
      key: 'splrName',
      type: 'input',
      props: { clearable: true, placeholder: '请输入供应商' }
    },
    {
      label: '二维码',
      key: 'qrcode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入二维码' }
    },
    {
      label: '条码',
      key: 'trackCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入条码' }
    },
    {
      label: '包装',
      key: 'packName',
      type: 'input',
      props: { clearable: true, placeholder: '请输入包装' }
    },
    {
      label: '批次',
      key: 'batch',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入批次'
      }
      props: { clearable: true, placeholder: '请输入批次' }
    },
    {
      label: '供应商批次',
      key: 'splrBatch',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入供应商批次'
      }
      props: { clearable: true, placeholder: '请输入供应商批次' }
    },
    {
      label: '字段索引',
      key: 'fieldsIndex',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入字段索引'
      }
      props: { clearable: true, placeholder: '请输入字段索引' }
    },
    {
      label: '备注',
      key: 'memo',
      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,
@@ -243,7 +311,12 @@
      apiParams: buildPreparationItemPageQueryParams(searchForm.value),
      immediate: false,
      paginationKey: getPreparationItemPaginationKey(),
      columnsFactory: () => createOutStockItemTableColumns({ handleActionClick: openDetail })
      columnsFactory: () =>
        createPreparationItemTableColumns({
          handleActionClick,
          canEdit: canUpdate.value,
          canDelete: canDelete.value
        })
    },
    transform: {
      dataTransformer: (records) =>
@@ -265,10 +338,132 @@
  }
  function handleReset() {
    const resetSeed =
      initialOrderId !== undefined ? { orderId: Number(initialOrderId) || '' } : {}
    const resetSeed = initialOrderId !== undefined ? { orderId: Number(initialOrderId) || '' } : {}
    Object.assign(searchForm.value, createPreparationItemSearchState(resetSeed))
    resetSearchParams(buildPreparationItemPageQueryParams(createPreparationItemSearchState(resetSeed)))
    resetSearchParams(
      buildPreparationItemPageQueryParams(createPreparationItemSearchState(resetSeed))
    )
  }
  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
    }
  }
  function openCreateDialog() {
    if (!canCreateItem.value) {
      return
    }
    dialogType.value = 'add'
    currentItemData.value = buildPreparationItemDialogModel({
      orderId: resolvedOrderId.value
    })
    dialogVisible.value = true
  }
  function openEditDialog(row) {
    dialogType.value = 'edit'
    currentItemData.value = buildPreparationItemDialogModel(row)
    dialogVisible.value = true
  }
  async function handleDelete(row) {
    await ElMessageBox.confirm(
      `确定删除备料明细 ${row.matnrCode || row.id || ''} 吗?`,
      '删除确认',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    await fetchDeletePreparationItem(row.id)
    ElMessage.success('备料明细已删除')
    await refreshData()
  }
  async function handleBatchDelete() {
    if (!selectedRows.value.length) {
      return
    }
    await ElMessageBox.confirm(
      `确定删除选中的 ${selectedRows.value.length} 条备料明细吗?`,
      '批量删除确认',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    await fetchDeletePreparationItem(selectedRows.value.map((item) => item.id))
    ElMessage.success('备料明细已批量删除')
    selectedRows.value = []
    await refreshData()
  }
  async function handleActionClick(action, row) {
    try {
      if (action.key === 'view') {
        await openDetail(row)
        return
      }
      if (action.key === 'edit') {
        openEditDialog(row)
        return
      }
      if (action.key === 'delete') {
        await handleDelete(row)
      }
    } catch (error) {
      if (error === 'cancel' || error?.message === 'cancel') {
        return
      }
      ElMessage.error(error?.message || '备料明细操作失败')
    }
  }
  async function handleDialogSubmit(payload) {
    submitLoading.value = true
    try {
      const requestPayload = buildPreparationItemSavePayload({
        ...payload,
        orderId: payload.orderId ?? resolvedOrderId.value
      })
      if (dialogType.value === 'edit') {
        await fetchUpdatePreparationItem(requestPayload)
        ElMessage.success('备料明细已更新')
      } else {
        await fetchSavePreparationItem(requestPayload)
        ElMessage.success('备料明细已新增')
      }
      dialogVisible.value = false
      await refreshData()
    } catch (error) {
      ElMessage.error(
        error?.message || (dialogType.value === 'edit' ? '备料明细更新失败' : '备料明细新增失败')
      )
    } finally {
      submitLoading.value = false
    }
  }
  function applyRouteSearch() {
rsf-design/src/views/orders/preparation-item/modules/preparation-item-dialog.vue
New file
@@ -0,0 +1,238 @@
<template>
  <ElDialog
    :model-value="visible"
    :title="dialogTitle"
    width="900px"
    destroy-on-close
    @update:model-value="handleVisibleChange"
    @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>
      <ElSpace>
        <ElButton @click="handleVisibleChange(false)">取消</ElButton>
        <ElButton type="primary" :loading="submitLoading" @click="handleSubmit">确定</ElButton>
      </ElSpace>
    </template>
  </ElDialog>
</template>
<script setup>
  import { computed, nextTick, reactive, ref, watch } from 'vue'
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import {
    buildPreparationItemDialogModel,
    createPreparationItemFormState,
    getPreparationItemStatusOptions
  } from '../preparationItemPage.helpers.js'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    dialogType: { type: String, default: 'add' },
    itemData: { type: Object, default: () => ({}) },
    orderId: { type: [Number, String], default: undefined },
    submitLoading: { type: Boolean, default: false }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const formRef = ref()
  const form = reactive(createPreparationItemFormState())
  const isEdit = computed(() => props.dialogType === 'edit')
  const dialogTitle = computed(() => (isEdit.value ? '编辑备料明细' : '新增备料明细'))
  const rules = computed(() => ({
    anfme: [{ required: true, message: '请输入计划数量', trigger: 'blur' }]
  }))
  const formItems = computed(() => [
    {
      label: '备料单ID',
      key: 'orderId',
      type: 'input',
      hidden: true,
      props: { disabled: true }
    },
    {
      label: '备料单号',
      key: 'orderCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入备料单号' }
    },
    {
      label: 'PO明细ID',
      key: 'poDetlId',
      type: 'input',
      props: { clearable: true, placeholder: '请输入 PO 明细ID' }
    },
    {
      label: '物料ID',
      key: 'matnrId',
      type: 'input',
      props: { clearable: true, placeholder: '请输入物料ID' }
    },
    {
      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, precision: 2, controlsPosition: 'right', placeholder: '请输入计划数量' }
    },
    {
      label: '库存单位',
      key: 'stockUnit',
      type: 'input',
      props: { clearable: true, placeholder: '请输入库存单位' }
    },
    {
      label: '采购数量',
      key: 'purQty',
      type: 'number',
      props: { min: 0, precision: 2, controlsPosition: 'right', placeholder: '请输入采购数量' }
    },
    {
      label: '采购单位',
      key: 'purUnit',
      type: 'input',
      props: { clearable: true, placeholder: '请输入采购单位' }
    },
    {
      label: '已出数量',
      key: 'qty',
      type: 'number',
      props: { min: 0, precision: 2, controlsPosition: 'right', placeholder: '请输入已出数量' }
    },
    {
      label: '供应商编码',
      key: 'splrCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入供应商编码' }
    },
    {
      label: '供应商',
      key: 'splrName',
      type: 'input',
      props: { clearable: true, placeholder: '请输入供应商' }
    },
    {
      label: '供应商批次',
      key: 'splrBatch',
      type: 'input',
      props: { clearable: true, placeholder: '请输入供应商批次' }
    },
    {
      label: '二维码',
      key: 'qrcode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入二维码' }
    },
    {
      label: '条码',
      key: 'trackCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入条码' }
    },
    {
      label: '包装',
      key: 'packName',
      type: 'input',
      props: { clearable: true, placeholder: '请输入包装' }
    },
    {
      label: '状态',
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: getPreparationItemStatusOptions(),
        placeholder: '请选择状态'
      }
    },
    {
      label: '备注',
      key: 'memo',
      type: 'input',
      span: 24,
      props: { type: 'textarea', rows: 3, clearable: true, placeholder: '请输入备注' }
    }
  ])
  function loadFormData() {
    Object.assign(
      form,
      buildPreparationItemDialogModel({
        ...props.itemData,
        orderId: props.itemData?.orderId ?? props.orderId
      })
    )
  }
  function resetForm() {
    Object.assign(form, createPreparationItemFormState(), {
      orderId: props.orderId
    })
    formRef.value?.clearValidate?.()
  }
  async function handleSubmit() {
    try {
      await formRef.value?.validate?.()
      emit('submit', { ...form })
    } catch {
      return
    }
  }
  function handleVisibleChange(value) {
    emit('update:visible', value)
  }
  function handleClosed() {
    resetForm()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (!visible) {
        return
      }
      loadFormData()
      nextTick(() => formRef.value?.clearValidate?.())
    },
    { immediate: true }
  )
  watch(
    () => props.itemData,
    () => {
      if (props.visible) {
        loadFormData()
      }
    },
    { deep: true }
  )
</script>
rsf-design/src/views/orders/preparation-item/preparationItemPage.helpers.js
@@ -18,14 +18,26 @@
    orderId: '',
    orderCode: '',
    poCode: '',
    poDetlId: '',
    matnrId: '',
    platItemId: '',
    matnrCode: '',
    maktx: '',
    anfme: '',
    stockUnit: '',
    purQty: '',
    purUnit: '',
    qty: '',
    splrCode: '',
    splrName: '',
    batch: '',
    splrBatch: '',
    trackCode: '',
    qrcode: '',
    packName: '',
    barcode: '',
    fieldsIndex: '',
    memo: '',
    status: '',
    ...seed
  }
@@ -42,6 +54,31 @@
  const result = {
    ...buildOutStockItemSearchParams(params)
  }
  ;[
    'poDetlId',
    'matnrId',
    'stockUnit',
    'purUnit',
    'splrCode',
    'splrName',
    'qrcode',
    'packName',
    'memo'
  ].forEach((key) => {
    const value = params[key]
    if (value !== '' && value !== undefined && value !== null) {
      result[key] = value
    }
  })
  ;['anfme', 'purQty', 'qty'].forEach((key) => {
    if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
      const numericValue = Number(params[key])
      if (!Number.isNaN(numericValue)) {
        result[key] = numericValue
      }
    }
  })
  if (params.orderId !== '' && params.orderId !== undefined && params.orderId !== null) {
    const numericValue = Number(params.orderId)
@@ -65,6 +102,81 @@
  return normalizeOutStockItemRow(record)
}
export function getPreparationItemStatusOptions() {
  return [
    { label: '正常', value: 1 },
    { label: '冻结', value: 0 }
  ]
}
export function createPreparationItemFormState() {
  return {
    id: undefined,
    orderId: undefined,
    orderCode: '',
    poDetlId: '',
    matnrId: '',
    matnrCode: '',
    maktx: '',
    anfme: undefined,
    stockUnit: '',
    purQty: undefined,
    purUnit: '',
    qty: undefined,
    splrCode: '',
    splrName: '',
    splrBatch: '',
    qrcode: '',
    trackCode: '',
    packName: '',
    memo: '',
    status: 1
  }
}
export function buildPreparationItemDialogModel(record = {}) {
  return {
    ...createPreparationItemFormState(),
    ...record
  }
}
export function buildPreparationItemSavePayload(formData = {}) {
  const payload = {}
  ;[
    'id',
    'orderId',
    'orderCode',
    'poDetlId',
    'matnrId',
    'matnrCode',
    'maktx',
    'stockUnit',
    'purUnit',
    'splrCode',
    'splrName',
    'splrBatch',
    'qrcode',
    'trackCode',
    'packName',
    'memo'
  ].forEach((key) => {
    if (formData[key] !== '' && formData[key] !== undefined && formData[key] !== null) {
      payload[key] = formData[key]
    }
  })
  ;['anfme', 'purQty', 'qty', 'status'].forEach((key) => {
    if (formData[key] !== '' && formData[key] !== undefined && formData[key] !== null) {
      const numericValue = Number(formData[key])
      if (!Number.isNaN(numericValue)) {
        payload[key] = numericValue
      }
    }
  })
  return payload
}
export function getPreparationItemReportColumns() {
  return [
    { key: 'orderCode', label: '备料单号' },
rsf-design/src/views/orders/preparation-item/preparationItemTable.columns.js
New file
@@ -0,0 +1,66 @@
import { h } from 'vue'
import { ElTag } from 'element-plus'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
function buildStatusTag(row) {
  return h(
    ElTag,
    { type: row.statusTagType || 'info', effect: 'light' },
    () => row.statusText || '--'
  )
}
export function createPreparationItemTableColumns({
  handleActionClick,
  canEdit = true,
  canDelete = true
} = {}) {
  return [
    { type: 'selection', width: 48, align: 'center' },
    { type: 'globalIndex', label: '序号', width: 72, align: 'center' },
    { prop: 'orderCode', label: '备料单号', minWidth: 150, showOverflowTooltip: true },
    { prop: 'poDetlId', label: 'PO明细ID', minWidth: 120, showOverflowTooltip: true },
    { prop: 'matnrId', label: '物料ID', minWidth: 120, showOverflowTooltip: true },
    { prop: 'matnrCode', label: '物料编码', minWidth: 150, showOverflowTooltip: true },
    { prop: 'maktx', label: '物料名称', minWidth: 220, showOverflowTooltip: true },
    { prop: 'anfme', label: '计划数量', width: 100, align: 'right' },
    { prop: 'stockUnit', label: '库存单位', width: 100, align: 'center' },
    { prop: 'purQty', label: '采购数量', width: 100, align: 'right' },
    { prop: 'purUnit', label: '采购单位', width: 100, align: 'center' },
    { prop: 'qty', label: '已出数量', width: 100, align: 'right' },
    { prop: 'splrCode', label: '供应商编码', minWidth: 120, showOverflowTooltip: true },
    { prop: 'splrName', label: '供应商', minWidth: 150, showOverflowTooltip: true },
    { prop: 'qrcode', label: '二维码', minWidth: 140, showOverflowTooltip: true },
    { prop: 'trackCode', label: '条码', minWidth: 140, showOverflowTooltip: true },
    { prop: 'packName', label: '包装', minWidth: 120, showOverflowTooltip: true },
    {
      prop: 'statusText',
      label: '状态',
      width: 96,
      align: 'center',
      formatter: (row) => buildStatusTag(row)
    },
    { prop: 'memo', label: '备注', minWidth: 150, showOverflowTooltip: true },
    {
      prop: 'operation',
      label: '操作',
      width: 120,
      fixed: 'right',
      formatter: (row) =>
        h(ArtButtonMore, {
          list: [
            { key: 'view', label: '查看', icon: 'ri:eye-line' },
            { key: 'edit', label: '编辑', icon: 'ri:edit-line', disabled: !canEdit },
            {
              key: 'delete',
              label: '删除',
              icon: 'ri:delete-bin-6-line',
              color: 'var(--el-color-danger)',
              disabled: !canDelete
            }
          ],
          onClick: (item) => handleActionClick?.(item, row)
        })
    }
  ]
}
rsf-design/src/views/orders/preparation/index.vue
@@ -12,6 +12,16 @@
      <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
        <template #left>
          <ElSpace wrap>
            <ElButton v-if="canCreate" type="primary" @click="generateDialogVisible = true">
              手动建单
            </ElButton>
            <ElButton
              v-if="canUpdate"
              :disabled="loading || selectedRows.length === 0"
              @click="waveDialogVisible = true"
            >
              生成波次
            </ElButton>
            <ListExportPrint
              :preview-visible="previewVisible"
              @update:previewVisible="handlePreviewVisibleChange"
@@ -52,13 +62,31 @@
      @size-change="handleDetailSizeChange"
      @current-change="handleDetailCurrentChange"
    />
    <PreparationGenerateDialog
      v-model:visible="generateDialogVisible"
      @created="handlePreparationCreated"
    />
    <PreparationWaveDialog
      v-model:visible="waveDialogVisible"
      :submitting="waveSubmitting"
      @submit="handleGenerateWave"
    />
    <PreparationTaskDialog
      v-model:visible="taskDialogVisible"
      :row="activeTaskRow"
      @submitted="handleTaskSubmitted"
    />
  </div>
</template>
<script setup>
  import { computed, reactive, ref } from 'vue'
  import { ElMessage, ElMessageBox } from 'element-plus'
  import { ElButton, ElMessage, ElMessageBox, ElSpace } from 'element-plus'
  import { useRouter } from 'vue-router'
  import { useAuth } from '@/hooks/core/useAuth'
  import { useUserStore } from '@/store/modules/user'
  import { useTable } from '@/hooks/core/useTable'
  import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
@@ -70,15 +98,20 @@
    fetchCompletePreparation,
    fetchDeletePreparation,
    fetchExportPreparationReport,
    fetchGeneratePreparationWave,
    fetchGetPreparationDetail,
    fetchGetPreparationMany,
    fetchPreparationItemPage,
    fetchPreparationPage
  } from '@/api/preparation'
  import PreparationDetailDrawer from './modules/preparation-detail-drawer.vue'
  import PreparationGenerateDialog from './modules/preparation-generate-dialog.vue'
  import PreparationTaskDialog from './modules/preparation-task-dialog.vue'
  import PreparationWaveDialog from './modules/preparation-wave-dialog.vue'
  import { createPreparationTableColumns } from './preparationTable.columns'
  import {
    buildPreparationDetailQueryParams,
    buildPreparationGenerateWavePayload,
    buildPreparationPageQueryParams,
    buildPreparationPrintRows,
    buildPreparationReportMeta,
@@ -93,6 +126,7 @@
  defineOptions({ name: 'Preparation' })
  const { hasAuth } = useAuth()
  const userStore = useUserStore()
  const router = useRouter()
  const reportTitle = PREPARATION_REPORT_TITLE
@@ -103,6 +137,11 @@
  const detailData = ref({})
  const detailTableData = ref([])
  const activePreparationId = ref(null)
  const generateDialogVisible = ref(false)
  const waveDialogVisible = ref(false)
  const waveSubmitting = ref(false)
  const taskDialogVisible = ref(false)
  const activeTaskRow = ref({})
  const detailPagination = reactive({
    current: 1,
@@ -110,6 +149,8 @@
    total: 0
  })
  const canCreate = computed(() => hasAuth('add'))
  const canUpdate = computed(() => hasAuth('update'))
  const reportQueryParams = computed(() => buildPreparationSearchParams(searchForm.value))
  const detailColumns = computed(() => createPreparationDetailItemColumns())
  const searchItems = computed(() => [
@@ -132,6 +173,12 @@
      props: { clearable: true, placeholder: '请输入 PO 单号' }
    },
    {
      label: 'PO ID',
      key: 'poId',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入 PO ID' }
    },
    {
      label: '业务类型',
      key: 'wkType',
      type: 'input',
@@ -140,20 +187,56 @@
    {
      label: '单据状态',
      key: 'exceStatus',
      type: 'input',
      props: { clearable: true, placeholder: '请输入状态' }
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入状态' }
    },
    {
      label: '释放状态',
      key: 'rleStatus',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', 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: 'logisNo',
      type: 'input',
      props: { clearable: true, placeholder: '请输入释放状态' }
      props: { clearable: true, placeholder: '请输入物流单号' }
    },
    {
      label: '到货日期',
      key: 'arrTime',
      type: 'date',
      props: { clearable: true, valueFormat: 'YYYY-MM-DD', 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: '请输入备注' }
    }
  ])
@@ -178,9 +261,13 @@
    detailLoading.value = true
    try {
      const [detailResponse, itemResponse] = await Promise.all([
        guardRequestWithMessage(fetchGetPreparationDetail(activePreparationId.value), {}, {
          timeoutMessage: '备料单详情加载超时,已停止等待'
        }),
        guardRequestWithMessage(
          fetchGetPreparationDetail(activePreparationId.value),
          {},
          {
            timeoutMessage: '备料单详情加载超时,已停止等待'
          }
        ),
        guardRequestWithMessage(
          fetchPreparationItemPage(
            buildPreparationDetailQueryParams({
@@ -239,6 +326,12 @@
            orderId: String(row.id)
          }
        })
        return
      }
      if (action.key === 'public') {
        activeTaskRow.value = row
        taskDialogVisible.value = true
        return
      }
@@ -329,6 +422,34 @@
    resetSearchParams()
  }
  async function handleGenerateWave(waveRuleId) {
    waveSubmitting.value = true
    try {
      await fetchGeneratePreparationWave(
        buildPreparationGenerateWavePayload(selectedRows.value, waveRuleId)
      )
      ElMessage.success('波次生成成功')
      waveDialogVisible.value = false
      selectedRows.value = []
      await refreshData()
    } catch (error) {
      ElMessage.error(error?.message || '波次生成失败')
    } finally {
      waveSubmitting.value = false
    }
  }
  async function handlePreparationCreated() {
    await refreshData()
  }
  async function handleTaskSubmitted() {
    await refreshData()
    if (detailDrawerVisible.value && activePreparationId.value === activeTaskRow.value?.id) {
      await loadDetailResources()
    }
  }
  function handleDetailSizeChange(size) {
    detailPagination.size = size
    detailPagination.current = 1
rsf-design/src/views/orders/preparation/modules/preparation-generate-dialog.vue
New file
@@ -0,0 +1,301 @@
<template>
  <ElDialog
    :model-value="visible"
    title="生成备料单"
    width="92%"
    top="4vh"
    destroy-on-close
    @update:model-value="handleVisibleChange"
  >
    <div class="flex flex-col gap-4">
      <ArtSearchBar
        v-model="searchForm"
        :items="searchItems"
        :show-expand="true"
        @search="handleSearch"
        @reset="handleReset"
      />
      <ElCard class="art-table-card">
        <template #header>
          <div class="flex items-center justify-between gap-3">
            <span class="font-medium">可选发货明细</span>
            <ElSpace>
              <ElButton @click="reloadData">刷新</ElButton>
              <ElButton type="primary" :disabled="!selectedRows.length" @click="openPreview">
                预览生成
              </ElButton>
            </ElSpace>
          </div>
        </template>
        <ArtTable
          :loading="loading"
          :data="tableData"
          :columns="tableColumns"
          :pagination="pagination"
          @selection-change="handleSelectionChange"
          @pagination:size-change="handleSizeChange"
          @pagination:current-change="handleCurrentChange"
        />
      </ElCard>
    </div>
    <ElDialog v-model="previewVisible" title="生成预览" width="88%" append-to-body destroy-on-close>
      <div class="flex flex-col gap-4">
        <div class="text-sm text-[var(--art-text-gray-600)]">
          已选择 {{ previewRows.length }} 条发货明细,可在下方调整本次备料数量。
        </div>
        <ElTable :data="previewRows" border height="56vh">
          <ElTableColumn type="index" label="序号" width="72" align="center" />
          <ElTableColumn
            prop="deliveryCode"
            label="发货单号"
            min-width="160"
            show-overflow-tooltip
          />
          <ElTableColumn prop="matnrCode" label="物料编码" min-width="150" show-overflow-tooltip />
          <ElTableColumn prop="maktx" label="物料名称" min-width="220" show-overflow-tooltip />
          <ElTableColumn prop="splrName" label="供应商" min-width="150" show-overflow-tooltip />
          <ElTableColumn prop="unit" label="单位" width="90" align="center" />
          <ElTableColumn label="剩余数量" width="110" align="right">
            <template #default="{ row }">
              {{ row.remainingQty }}
            </template>
          </ElTableColumn>
          <ElTableColumn label="本次备料数量" width="160" align="center">
            <template #default="{ row }">
              <ElInputNumber
                v-model="row.anfme"
                :min="0"
                :max="row.remainingQty"
                :precision="2"
                controls-position="right"
              />
            </template>
          </ElTableColumn>
          <ElTableColumn
            prop="splrBatch"
            label="供应商批次"
            min-width="140"
            show-overflow-tooltip
          />
          <ElTableColumn prop="updateTime" label="更新时间" min-width="170" show-overflow-tooltip />
        </ElTable>
      </div>
      <template #footer>
        <ElSpace>
          <ElButton @click="previewVisible = false">取消</ElButton>
          <ElButton type="primary" :loading="submitting" @click="handleConfirmGenerate">
            生成备料单
          </ElButton>
        </ElSpace>
      </template>
    </ElDialog>
    <template #footer>
      <ElButton @click="handleVisibleChange(false)">关闭</ElButton>
    </template>
  </ElDialog>
</template>
<script setup>
  import { computed, reactive, ref, watch } from 'vue'
  import { ElMessage } from 'element-plus'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import { fetchPreparationDialogPage, fetchGeneratePreparationOrders } from '@/api/preparation'
  const props = defineProps({
    visible: { type: Boolean, default: false }
  })
  const emit = defineEmits(['update:visible', 'created'])
  const loading = ref(false)
  const submitting = ref(false)
  const tableData = ref([])
  const selectedRows = ref([])
  const previewVisible = ref(false)
  const previewRows = ref([])
  const searchForm = ref(createSearchState())
  const pagination = reactive({
    current: 1,
    size: 20,
    total: 0
  })
  const searchItems = computed(() => [
    {
      label: '关键字',
      key: 'condition',
      type: 'input',
      props: { clearable: true, placeholder: '请输入发货单号/物料编码/物料名称' }
    },
    {
      label: '发货单号',
      key: 'deliveryCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入发货单号' }
    },
    {
      label: '物料名称',
      key: 'maktx',
      type: 'input',
      props: { clearable: true, placeholder: '请输入物料名称' }
    },
    {
      label: '物料编码',
      key: 'matnrCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入物料编码' }
    },
    {
      label: '供应商',
      key: 'splrName',
      type: 'input',
      props: { clearable: true, placeholder: '请输入供应商' }
    }
  ])
  const tableColumns = [
    { type: 'selection', width: 48, align: 'center' },
    { type: 'globalIndex', label: '序号', width: 72, align: 'center' },
    { prop: 'deliveryCode', label: '发货单号', minWidth: 160, showOverflowTooltip: true },
    { prop: 'matnrCode', label: '物料编码', minWidth: 150, showOverflowTooltip: true },
    { prop: 'maktx', label: '物料名称', minWidth: 220, showOverflowTooltip: true },
    { prop: 'unit', label: '单位', width: 90, align: 'center' },
    { prop: 'anfme', label: '计划数量', width: 100, align: 'right' },
    { prop: 'workQty', label: '执行数量', width: 100, align: 'right' },
    { prop: 'qty', label: '已出数量', width: 100, align: 'right' },
    { prop: 'splrName', label: '供应商', minWidth: 150, showOverflowTooltip: true },
    { prop: 'splrBatch', label: '供应商批次', minWidth: 140, showOverflowTooltip: true },
    { prop: 'updateTime', label: '更新时间', minWidth: 170, showOverflowTooltip: true }
  ]
  function createSearchState() {
    return {
      condition: '',
      deliveryCode: '',
      maktx: '',
      matnrCode: '',
      splrName: ''
    }
  }
  function buildQueryParams() {
    return {
      current: pagination.current,
      pageSize: pagination.size,
      ...Object.fromEntries(
        Object.entries(searchForm.value).filter(([, value]) => value !== '' && value !== undefined)
      )
    }
  }
  function normalizeRow(row = {}) {
    const anfme = Number(row.anfme || 0)
    const workQty = Number(row.workQty || 0)
    const qty = Number(row.qty || 0)
    const remainingQty = Math.max(anfme - workQty - qty, 0)
    return {
      ...row,
      remainingQty,
      anfme: remainingQty
    }
  }
  async function loadData() {
    loading.value = true
    try {
      const response = await fetchPreparationDialogPage(buildQueryParams())
      const normalized = defaultResponseAdapter(response)
      tableData.value = Array.isArray(normalized.records) ? normalized.records : []
      pagination.total = Number(normalized.total || 0)
      pagination.current = Number(normalized.current || pagination.current || 1)
      pagination.size = Number(normalized.size || pagination.size || 20)
    } catch (error) {
      tableData.value = []
      pagination.total = 0
      ElMessage.error(error?.message || '发货明细加载失败')
    } finally {
      loading.value = false
    }
  }
  function reloadData() {
    loadData()
  }
  function handleSelectionChange(rows) {
    selectedRows.value = Array.isArray(rows) ? rows : []
  }
  function handleSearch(params) {
    searchForm.value = { ...searchForm.value, ...params }
    pagination.current = 1
    loadData()
  }
  function handleReset() {
    searchForm.value = createSearchState()
    pagination.current = 1
    loadData()
  }
  function handleSizeChange(size) {
    pagination.size = size
    pagination.current = 1
    loadData()
  }
  function handleCurrentChange(current) {
    pagination.current = current
    loadData()
  }
  function openPreview() {
    previewRows.value = selectedRows.value.map((row) => normalizeRow({ ...row }))
    previewVisible.value = true
  }
  async function handleConfirmGenerate() {
    const rows = previewRows.value.filter((row) => Number(row.anfme) > 0)
    if (!rows.length) {
      ElMessage.warning('至少保留一条数量大于 0 的发货明细')
      return
    }
    submitting.value = true
    try {
      await fetchGeneratePreparationOrders({ ids: rows })
      ElMessage.success('备料单生成成功')
      previewVisible.value = false
      emit('created')
      handleVisibleChange(false)
    } catch (error) {
      ElMessage.error(error?.message || '备料单生成失败')
    } finally {
      submitting.value = false
    }
  }
  function handleVisibleChange(value) {
    emit('update:visible', value)
  }
  watch(
    () => props.visible,
    (visible) => {
      if (!visible) {
        selectedRows.value = []
        previewRows.value = []
        previewVisible.value = false
        return
      }
      loadData()
    }
  )
</script>
rsf-design/src/views/orders/preparation/modules/preparation-task-dialog.vue
New file
@@ -0,0 +1,259 @@
<template>
  <ElDialog
    :model-value="visible"
    title="下发执行"
    width="94%"
    top="4vh"
    destroy-on-close
    @update:model-value="handleVisibleChange"
  >
    <div class="flex flex-col gap-4">
      <ElCard shadow="never">
        <div class="grid gap-4 md:grid-cols-[240px_240px_auto]">
          <div>
            <div class="mb-2 text-sm text-[var(--art-text-gray-600)]">波次策略</div>
            <ElSelect
              v-model="waveRuleId"
              class="w-full"
              filterable
              clearable
              placeholder="请选择波次策略"
              :loading="metaLoading"
              @change="handleWaveRuleChange"
            >
              <ElOption
                v-for="item in waveRuleOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </ElSelect>
          </div>
          <div>
            <div class="mb-2 text-sm text-[var(--art-text-gray-600)]">批量设置站点</div>
            <ElSelect
              v-model="batchSiteNo"
              class="w-full"
              filterable
              clearable
              placeholder="请选择站点"
            >
              <ElOption
                v-for="site in siteOptions"
                :key="site.value"
                :label="site.label"
                :value="site.value"
              />
            </ElSelect>
          </div>
          <div class="flex items-end gap-2">
            <ElButton :loading="previewLoading" @click="loadTaskPreview">刷新预览</ElButton>
            <ElButton :disabled="!selectedRowIds.length || !batchSiteNo" @click="applyBatchSite">
              应用到选中行
            </ElButton>
          </div>
        </div>
      </ElCard>
      <ElTable :data="taskRows" border height="58vh" @selection-change="handleSelectionChange">
        <ElTableColumn type="selection" width="48" align="center" />
        <ElTableColumn type="index" label="序号" width="72" align="center" />
        <ElTableColumn prop="locCode" label="库位" min-width="120" show-overflow-tooltip />
        <ElTableColumn prop="barcode" label="容器" min-width="120" show-overflow-tooltip />
        <ElTableColumn prop="matnrCode" label="物料编码" min-width="150" show-overflow-tooltip />
        <ElTableColumn prop="batch" label="批次" min-width="120" show-overflow-tooltip />
        <ElTableColumn prop="unit" label="单位" width="90" align="center" />
        <ElTableColumn prop="outQty" label="出库数量" width="110" align="right" />
        <ElTableColumn prop="anfme" label="库存数量" width="110" align="right" />
        <ElTableColumn label="出库口" width="180">
          <template #default="{ row }">
            <ElSelect
              v-model="row.siteNo"
              class="w-full"
              filterable
              clearable
              placeholder="请选择站点"
            >
              <ElOption
                v-for="site in resolveSiteOptions(row)"
                :key="site.value"
                :label="site.label"
                :value="site.value"
              />
            </ElSelect>
          </template>
        </ElTableColumn>
      </ElTable>
    </div>
    <template #footer>
      <ElSpace>
        <ElButton @click="handleVisibleChange(false)">关闭</ElButton>
        <ElButton type="primary" :loading="submitting" @click="handleSubmit"> 生成任务 </ElButton>
      </ElSpace>
    </template>
  </ElDialog>
</template>
<script setup>
  import { ref, watch } from 'vue'
  import { ElMessage } from 'element-plus'
  import { fetchWaveRulePage } from '@/api/system-manage'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import {
    fetchGeneratePreparationTasks,
    fetchPreparationTaskPreview,
    fetchPreparationTaskSites
  } from '@/api/preparation'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    row: { type: Object, default: () => ({}) }
  })
  const emit = defineEmits(['update:visible', 'submitted'])
  const metaLoading = ref(false)
  const previewLoading = ref(false)
  const submitting = ref(false)
  const waveRuleId = ref()
  const waveRuleOptions = ref([])
  const siteOptions = ref([])
  const batchSiteNo = ref('')
  const selectedRowIds = ref([])
  const taskRows = ref([])
  function handleVisibleChange(value) {
    emit('update:visible', value)
  }
  function normalizeSiteOptions(records = []) {
    return (Array.isArray(records) ? records : []).map((item) => ({
      label: item.site || item.staNo || item.name || '--',
      value: item.site || item.staNo || item.name || '',
      raw: item
    }))
  }
  async function loadMeta() {
    metaLoading.value = true
    try {
      const [waveResponse, siteResponse] = await Promise.all([
        fetchWaveRulePage({ current: 1, pageSize: 200, status: 1 }),
        fetchPreparationTaskSites()
      ])
      const waveRecords = defaultResponseAdapter(waveResponse).records
      waveRuleOptions.value = waveRecords.map((item) => ({
        label: item.name || item.code || `波次策略${item.id}`,
        value: Number(item.id)
      }))
      if (!waveRuleId.value && waveRuleOptions.value.length) {
        waveRuleId.value = waveRuleOptions.value[0].value
      }
      siteOptions.value = normalizeSiteOptions(siteResponse)
    } catch (error) {
      waveRuleOptions.value = []
      siteOptions.value = []
      ElMessage.error(error?.message || '下发执行初始化失败')
    } finally {
      metaLoading.value = false
    }
  }
  function normalizeTaskRow(row = {}) {
    return {
      ...row,
      siteNo: row.siteNo || row.site || ''
    }
  }
  async function loadTaskPreview() {
    if (!props.row?.id) {
      return
    }
    if (!waveRuleId.value) {
      ElMessage.warning('请选择波次策略')
      return
    }
    previewLoading.value = true
    try {
      const response = await fetchPreparationTaskPreview({
        orderId: Number(props.row.id),
        waveId: Number(waveRuleId.value)
      })
      taskRows.value = Array.isArray(response) ? response.map((item) => normalizeTaskRow(item)) : []
      selectedRowIds.value = []
    } catch (error) {
      taskRows.value = []
      ElMessage.error(error?.message || '任务预览加载失败')
    } finally {
      previewLoading.value = false
    }
  }
  function handleWaveRuleChange() {
    if (props.visible) {
      loadTaskPreview()
    }
  }
  function handleSelectionChange(rows) {
    selectedRowIds.value = Array.isArray(rows) ? rows.map((item) => item.id) : []
  }
  function applyBatchSite() {
    taskRows.value = taskRows.value.map((item) =>
      selectedRowIds.value.includes(item.id) ? { ...item, siteNo: batchSiteNo.value } : item
    )
  }
  function resolveSiteOptions(row) {
    if (Array.isArray(row?.staNos) && row.staNos.length) {
      return row.staNos.map((item) => ({
        label: item.staNo || item.site || '--',
        value: item.staNo || item.site || ''
      }))
    }
    return siteOptions.value
  }
  async function handleSubmit() {
    const items = taskRows.value.filter((item) => item.locCode && item.siteNo)
    if (!items.length) {
      ElMessage.warning('请至少为一条有库位的记录指定站点')
      return
    }
    submitting.value = true
    try {
      await fetchGeneratePreparationTasks({
        outId: Number(props.row.id),
        items
      })
      ElMessage.success('任务生成成功')
      emit('submitted')
      handleVisibleChange(false)
    } catch (error) {
      ElMessage.error(error?.message || '任务生成失败')
    } finally {
      submitting.value = false
    }
  }
  watch(
    () => props.visible,
    async (visible) => {
      if (!visible) {
        taskRows.value = []
        selectedRowIds.value = []
        batchSiteNo.value = ''
        return
      }
      await loadMeta()
      await loadTaskPreview()
    }
  )
</script>
rsf-design/src/views/orders/preparation/modules/preparation-wave-dialog.vue
New file
@@ -0,0 +1,97 @@
<template>
  <ElDialog
    :model-value="visible"
    title="生成波次"
    width="520px"
    destroy-on-close
    @update:model-value="handleVisibleChange"
  >
    <ElForm label-width="110px">
      <ElFormItem label="波次策略" required>
        <ElSelect
          v-model="waveRuleId"
          class="w-full"
          filterable
          clearable
          placeholder="请选择波次策略"
          :loading="loading"
        >
          <ElOption
            v-for="item in waveRuleOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </ElSelect>
      </ElFormItem>
    </ElForm>
    <template #footer>
      <ElSpace>
        <ElButton @click="handleVisibleChange(false)">取消</ElButton>
        <ElButton type="primary" :loading="submitting" @click="handleSubmit">确定</ElButton>
      </ElSpace>
    </template>
  </ElDialog>
</template>
<script setup>
  import { ref, watch } from 'vue'
  import { ElMessage } from 'element-plus'
  import { fetchWaveRulePage } from '@/api/system-manage'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    submitting: { type: Boolean, default: false }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const loading = ref(false)
  const waveRuleId = ref()
  const waveRuleOptions = ref([])
  async function loadWaveRuleOptions() {
    loading.value = true
    try {
      const response = await fetchWaveRulePage({ current: 1, pageSize: 200, status: 1 })
      const normalized = defaultResponseAdapter(response)
      const records = Array.isArray(normalized.records) ? normalized.records : []
      waveRuleOptions.value = records.map((item) => ({
        label: item.name || item.code || `波次策略${item.id}`,
        value: Number(item.id)
      }))
      if (!waveRuleId.value && waveRuleOptions.value.length) {
        waveRuleId.value = waveRuleOptions.value[0].value
      }
    } catch (error) {
      waveRuleOptions.value = []
      ElMessage.error(error?.message || '波次策略加载失败')
    } finally {
      loading.value = false
    }
  }
  function handleVisibleChange(value) {
    emit('update:visible', value)
  }
  function handleSubmit() {
    if (!waveRuleId.value) {
      ElMessage.warning('请选择波次策略')
      return
    }
    emit('submit', waveRuleId.value)
  }
  watch(
    () => props.visible,
    (visible) => {
      if (!visible) {
        return
      }
      loadWaveRuleOptions()
    }
  )
</script>
rsf-design/src/views/orders/preparation/preparationPage.helpers.js
@@ -37,30 +37,38 @@
function normalizeStatusMeta(exceStatus, exceStatusText) {
  if (exceStatusText) {
    return PREPARATION_STATUS_META[Number(exceStatus)] || {
      text: exceStatusText,
      type: 'info'
    }
    return (
      PREPARATION_STATUS_META[Number(exceStatus)] || {
        text: exceStatusText,
        type: 'info'
      }
    )
  }
  return PREPARATION_STATUS_META[Number(exceStatus)] || {
    text: normalizeText(exceStatus) || '--',
    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: rleStatusText,
        type: 'info'
      }
    )
  }
  return PREPARATION_RLE_STATUS_META[Number(rleStatus)] || {
    text: normalizeText(rleStatus) || '--',
    type: 'info'
  }
  return (
    PREPARATION_RLE_STATUS_META[Number(rleStatus)] || {
      text: normalizeText(rleStatus) || '--',
      type: 'info'
    }
  )
}
export function createPreparationSearchState() {
@@ -68,10 +76,14 @@
    condition: '',
    code: '',
    poCode: '',
    poId: '',
    wkType: '',
    exceStatus: '',
    rleStatus: '',
    anfme: '',
    qty: '',
    logisNo: '',
    arrTime: '',
    customerName: '',
    saleOrgName: '',
    memo: ''
@@ -87,6 +99,7 @@
    'poCode',
    'wkType',
    'logisNo',
    'arrTime',
    'customerName',
    'saleOrgName',
    'memo'
@@ -104,6 +117,12 @@
  if (params.rleStatus !== '' && params.rleStatus !== undefined && params.rleStatus !== null) {
    result.rleStatus = normalizeNumber(params.rleStatus)
  }
  ;['poId', 'anfme', 'qty'].forEach((key) => {
    if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
      result[key] = normalizeNumber(params[key])
    }
  })
  return result
}
@@ -166,7 +185,8 @@
    memo: normalizeText(record.memo) || '--',
    canComplete: Number(record.exceStatus) !== 15,
    canCancel: Number(record.exceStatus) === 10,
    canDelete: Number(record.exceStatus) !== 15
    canDelete: Number(record.exceStatus) !== 15,
    canPublic: Number(record.workQty || 0) < Number(record.anfme || 0)
  }
}
@@ -189,6 +209,13 @@
  return [
    { key: 'view', label: $t('common.actions.detail'), icon: 'ri:eye-line' },
    { key: 'items', label: $t('common.actions.items'), icon: 'ri:list-check-3' },
    {
      key: 'public',
      label: '下发执行',
      icon: 'ri:send-plane-line',
      color: 'var(--el-color-primary)',
      disabled: !normalizedRow.canPublic
    },
    { key: 'print', label: $t('common.actions.print'), icon: 'ri:printer-line' },
    {
      key: 'complete',
@@ -251,3 +278,12 @@
    }
  }
}
export function buildPreparationGenerateWavePayload(rows = [], waveRuleId) {
  return {
    ids: Array.isArray(rows)
      ? rows.map((row) => Number(row?.id)).filter((id) => Number.isFinite(id))
      : [],
    waveRuleId: normalizeNumber(waveRuleId)
  }
}
rsf-design/src/views/orders/preparation/preparationTable.columns.js
@@ -7,12 +7,17 @@
  return [
    { type: 'selection', width: 48, align: 'center' },
    { type: 'globalIndex', label: '序号', width: 72, align: 'center' },
    { prop: 'id', label: 'ID', width: 90, 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: 'saleUserName', label: '销售员', minWidth: 120, showOverflowTooltip: true },
    { prop: 'businessTimeText', label: '出库日期', minWidth: 150, showOverflowTooltip: true },
    { prop: 'customerId', label: '客户编码', minWidth: 140, showOverflowTooltip: true },
    { prop: 'customerName', label: '客户', minWidth: 160, showOverflowTooltip: true },
    { prop: 'saleOrgName', label: '销售组织', minWidth: 150, showOverflowTooltip: true },
    { prop: 'stockOrgName', 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' },
@@ -39,11 +44,15 @@
          () => row.exceStatusText || '--'
        )
    },
    { 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: 150, showOverflowTooltip: true },
    {
      prop: 'operation',
      label: '操作',
      width: 120,
      width: 140,
      align: 'center',
      fixed: 'right',
      formatter: (row) =>