zhou zhou
昨天 d4e039545c9e97347223eb415fbba85ee01bc263
#页面优化
3个文件已添加
17个文件已修改
2311 ■■■■■ 已修改文件
rsf-design/build/manualChunks.js 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/task-item-log.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/task-log.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/transfer.js 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/en.json 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/zh.json 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task-item-log/taskItemLogPage.helpers.js 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task-item-log/taskItemLogTable.columns.js 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task-log/index.vue 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task-log/modules/task-item-log-panel.vue 339 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task-log/modules/task-log-detail-drawer.vue 92 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task-log/taskLogPage.helpers.js 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/task-log/taskLogTable.columns.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/transfer/index.vue 252 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/transfer/modules/transfer-detail-drawer.vue 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/transfer/modules/transfer-dialog.vue 342 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/transfer/modules/transfer-material-dialog.vue 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/transfer/modules/transfer-orders-panel.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/transfer/transferPage.helpers.js 336 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/transfer/transferTable.columns.js 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/build/manualChunks.js
@@ -16,12 +16,22 @@
    packages: ['xgplayer/']
  },
  {
    name: 'vendor-element-plus',
    packages: ['element-plus/', '@element-plus/']
  },
  {
    name: 'vendor-vue',
    packages: ['vue-router/', 'pinia/', '@vueuse/']
    // Keep Vue runtime, router/store, vueuse, and Element Plus together.
    // Splitting these into separate chunks can create circular chunk imports
    // after Rollup optimization, which then crashes at runtime with
    // "Cannot access 'x' before initialization".
    name: 'vendor-framework',
    packages: [
      'vue/',
      '@vue/',
      'vue-router/',
      'pinia/',
      'vue-demi/',
      '@vueuse/',
      'element-plus/',
      '@element-plus/',
      '@floating-ui/'
    ]
  },
  {
    name: 'vendor-utils',
rsf-design/src/api/task-item-log.js
@@ -34,6 +34,10 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy:
      typeof params.orderBy === 'string' && params.orderBy.trim()
        ? params.orderBy.trim()
        : 'create_time desc',
    ...filterParams(params, ['current', 'pageSize', 'size'])
  }
}
rsf-design/src/api/task-log.js
@@ -34,6 +34,10 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy:
      typeof params.orderBy === 'string' && params.orderBy.trim()
        ? params.orderBy.trim()
        : 'create_time desc',
    ...filterParams(params, ['current', 'pageSize', 'size'])
  }
}
rsf-design/src/api/transfer.js
@@ -23,11 +23,31 @@
export function buildTransferSearchParams(params = {}) {
  const result = {}
  ;['condition', 'code', 'orgWareName', 'tarWareName', 'orgAreaName', 'tarAreaName', 'memo', 'timeStart', 'timeEnd'].forEach((key) => {
  ;[
    'condition',
    'code',
    'orgWareName',
    'tarWareName',
    'orgAreaName',
    'tarAreaName',
    'memo',
    'timeStart',
    'timeEnd',
    'orderBy'
  ].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) result[key] = value
  })
  ;['type', 'source', 'exceStatus', 'status', 'orgWareId', 'tarWareId', 'orgAreaId', 'tarAreaId'].forEach((key) => {
  ;[
    'type',
    'source',
    'exceStatus',
    'status',
    'orgWareId',
    'tarWareId',
    'orgAreaId',
    'tarAreaId'
  ].forEach((key) => {
    if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
      result[key] = normalizeNumber(params[key])
    }
@@ -39,16 +59,31 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildTransferSearchParams(params)
  }
}
export function buildTransferLocsItemPageParams(params = {}) {
  return {
    orgAreaId: normalizeNumber(params.orgAreaId, void 0),
    matnrCode: normalizeText(params.matnrCode || params.code || ''),
    maktx: normalizeText(params.maktx || params.name || ''),
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20
  }
}
export function buildTransferOrderPageParams(params = {}) {
  return {
    ...(normalizeNumber(params.id, void 0) !== void 0
      ? { id: normalizeNumber(params.id, void 0) }
      : {}),
    condition: normalizeText(params.code || params.condition),
    code: normalizeText(params.code || params.condition),
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc'
  }
}
@@ -67,8 +102,14 @@
  const tarAreaId = normalizeNumber(formData.tarAreaId, void 0)
  const orgArea = optionMap.get(orgAreaId) || {}
  const tarArea = optionMap.get(tarAreaId) || {}
  const orgWareId = normalizeNumber(orgArea.warehouseId ?? orgArea.warehouse_id ?? orgArea.warehouseIdValue, void 0)
  const tarWareId = normalizeNumber(tarArea.warehouseId ?? tarArea.warehouse_id ?? tarArea.warehouseIdValue, void 0)
  const orgWareId = normalizeNumber(
    orgArea.warehouseId ?? orgArea.warehouse_id ?? orgArea.warehouseIdValue,
    void 0
  )
  const tarWareId = normalizeNumber(
    tarArea.warehouseId ?? tarArea.warehouse_id ?? tarArea.warehouseIdValue,
    void 0
  )
  return {
    ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
@@ -82,10 +123,18 @@
    ...(tarAreaId !== void 0 ? { tarAreaId } : {}),
    ...(orgWareId !== void 0 ? { orgWareId } : {}),
    ...(tarWareId !== void 0 ? { tarWareId } : {}),
    ...(normalizeText(orgArea.name || orgArea.areaName) ? { orgAreaName: normalizeText(orgArea.name || orgArea.areaName) } : {}),
    ...(normalizeText(tarArea.name || tarArea.areaName) ? { tarAreaName: normalizeText(tarArea.name || tarArea.areaName) } : {}),
    ...(normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) ? { orgWareName: normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) } : {}),
    ...(normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) ? { tarWareName: normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) } : {}),
    ...(normalizeText(orgArea.name || orgArea.areaName)
      ? { orgAreaName: normalizeText(orgArea.name || orgArea.areaName) }
      : {}),
    ...(normalizeText(tarArea.name || tarArea.areaName)
      ? { tarAreaName: normalizeText(tarArea.name || tarArea.areaName) }
      : {}),
    ...(normalizeText(orgArea.warehouseId$ || orgArea.warehouseName)
      ? { orgWareName: normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) }
      : {}),
    ...(normalizeText(tarArea.warehouseId$ || tarArea.warehouseName)
      ? { tarWareName: normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) }
      : {}),
    ...(formData.status !== undefined && formData.status !== null && formData.status !== ''
      ? { status: normalizeNumber(formData.status) }
      : { status: 1 }),
@@ -117,14 +166,36 @@
  return request.post({ url: '/transfer/update', params: payload })
}
export function fetchSaveTransferItems(payload = {}) {
  return request.post({ url: '/transfer/items/save', params: payload })
}
export function fetchUpdateTransferItems(payload = {}) {
  return request.post({ url: '/transfer/items/update', params: payload })
}
export function fetchTransferOrdersPage(params = {}) {
  return request.post({ url: '/transfer/orders/page', params: buildTransferOrderPageParams(params) })
  return request.post({
    url: '/transfer/orders/page',
    params: buildTransferOrderPageParams(params)
  })
}
export function fetchTransferLocsItemsPage(params = {}) {
  return request.post({
    url: '/transfer/locs/items',
    params: buildTransferLocsItemPageParams(params)
  })
}
export function fetchTransferPubOutStock(payload = {}) {
  return request.post({ url: '/transfer/pub/outStock', params: payload })
}
export function fetchEnabledTransferFields() {
  return request.get({ url: '/fields/enable/list' })
}
export async function fetchExportTransferReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/transfer/export`, {
    method: 'POST',
rsf-design/src/locales/langs/en.json
@@ -1881,17 +1881,23 @@
        "search": {
          "condition": "Keyword",
          "conditionPlaceholder": "Enter No./remark/warehouse/area",
          "timeStart": "Start Time",
          "timeEnd": "End Time",
          "code": "Transfer No.",
          "codePlaceholder": "Enter transfer No.",
          "type": "Transfer Type",
          "source": "Source",
          "exceStatus": "Execution Status",
          "orgWareId": "Source Warehouse ID",
          "orgWareName": "Source Warehouse",
          "orgWareNamePlaceholder": "Enter source warehouse",
          "tarWareId": "Target Warehouse ID",
          "tarWareName": "Target Warehouse",
          "tarWareNamePlaceholder": "Enter target warehouse",
          "orgAreaId": "Source Area ID",
          "orgAreaName": "Source Area",
          "orgAreaNamePlaceholder": "Enter source area",
          "tarAreaId": "Target Area ID",
          "tarAreaName": "Target Area",
          "tarAreaNamePlaceholder": "Enter target area",
          "status": "Status",
@@ -1920,9 +1926,13 @@
        "placeholder": {
          "condition": "Enter No./remark/warehouse/area",
          "code": "Enter transfer No.",
          "orgWareId": "Enter source warehouse ID",
          "orgWareName": "Enter source warehouse",
          "tarWareId": "Enter target warehouse ID",
          "tarWareName": "Enter target warehouse",
          "orgAreaId": "Enter source area ID",
          "orgAreaName": "Enter source area",
          "tarAreaId": "Enter target area ID",
          "tarAreaName": "Enter target area",
          "memo": "Enter remark"
        },
@@ -1968,15 +1978,36 @@
          "placeholderTarAreaId": "Please select a target area",
          "placeholderStatus": "Please select a status",
          "placeholderMemo": "Please enter a remark",
          "addMaterial": "Add Material",
          "deleteSelected": "Delete Selected",
          "itemCount": "Items: {count}",
          "supplierCode": "Supplier Code",
          "supplierName": "Supplier Name",
          "materialDuplicate": "The selected material already exists",
          "validation": {
            "type": "Please select a transfer type",
            "orgAreaId": "Please select a source area",
            "tarAreaId": "Please select a target area"
            "tarAreaId": "Please select a target area",
            "items": "Please add at least one transfer item",
            "anfme": "Transfer quantity must be greater than 0"
          }
        },
        "materialDialog": {
          "title": "Select Transfer Materials",
          "selected": "Added",
          "unselected": "Not Added",
          "table": {
            "status": "Status"
          },
          "placeholder": {
            "maktx": "Enter material name",
            "matnrCode": "Enter material code"
          }
        },
        "messages": {
          "detailTimeout": "Transfer detail timed out and waiting has stopped",
          "ordersTimeout": "Transfer items timed out and waiting has stopped",
          "itemsTimeout": "Transfer edit items timed out and waiting has stopped",
          "ordersLoadFailed": "Failed to load transfer items",
          "detailLoadFailed": "Failed to load transfer detail",
          "publishConfirm": "Are you sure you want to dispatch transfer order \"{code}\"?",
@@ -1984,7 +2015,8 @@
          "publishSuccess": "Dispatched successfully",
          "publishFailed": "Dispatch failed",
          "typeOptionsTimeout": "Transfer type options timed out and waiting has stopped",
          "areaOptionsTimeout": "Area options timed out and waiting has stopped"
          "areaOptionsTimeout": "Area options timed out and waiting has stopped",
          "fieldTimeout": "Field definitions timed out and waiting has stopped"
        }
      },
      "transferItem": {
rsf-design/src/locales/langs/zh.json
@@ -1889,17 +1889,23 @@
        "search": {
          "condition": "关键字",
          "conditionPlaceholder": "请输入单号/备注/仓库/库区",
          "timeStart": "开始时间",
          "timeEnd": "结束时间",
          "code": "调拨单号",
          "codePlaceholder": "请输入调拨单号",
          "type": "调拨类型",
          "source": "来源",
          "exceStatus": "执行状态",
          "orgWareId": "源仓库ID",
          "orgWareName": "源仓库",
          "orgWareNamePlaceholder": "请输入源仓库",
          "tarWareId": "目标仓库ID",
          "tarWareName": "目标仓库",
          "tarWareNamePlaceholder": "请输入目标仓库",
          "orgAreaId": "源库区ID",
          "orgAreaName": "源库区",
          "orgAreaNamePlaceholder": "请输入源库区",
          "tarAreaId": "目标库区ID",
          "tarAreaName": "目标库区",
          "tarAreaNamePlaceholder": "请输入目标库区",
          "status": "状态",
@@ -1928,9 +1934,13 @@
        "placeholder": {
          "condition": "请输入单号/备注/仓库/库区",
          "code": "请输入调拨单号",
          "orgWareId": "请输入源仓库ID",
          "orgWareName": "请输入源仓库",
          "tarWareId": "请输入目标仓库ID",
          "tarWareName": "请输入目标仓库",
          "orgAreaId": "请输入源库区ID",
          "orgAreaName": "请输入源库区",
          "tarAreaId": "请输入目标库区ID",
          "tarAreaName": "请输入目标库区",
          "memo": "请输入备注"
        },
@@ -1976,15 +1986,36 @@
          "placeholderTarAreaId": "请选择目标库区",
          "placeholderStatus": "请选择状态",
          "placeholderMemo": "请输入备注",
          "addMaterial": "新增物料",
          "deleteSelected": "删除选中",
          "itemCount": "明细数:{count}",
          "supplierCode": "供应商编码",
          "supplierName": "供应商名称",
          "materialDuplicate": "所选物料已存在,无需重复添加",
          "validation": {
            "type": "请选择调拨类型",
            "orgAreaId": "请选择源库区",
            "tarAreaId": "请选择目标库区"
            "tarAreaId": "请选择目标库区",
            "items": "请至少添加一条调拨明细",
            "anfme": "调拨数量必须大于 0"
          }
        },
        "materialDialog": {
          "title": "选择调拨物料",
          "selected": "已添加",
          "unselected": "未添加",
          "table": {
            "status": "状态"
          },
          "placeholder": {
            "maktx": "请输入物料名称",
            "matnrCode": "请输入物料编码"
          }
        },
        "messages": {
          "detailTimeout": "调拨单详情加载超时,已停止等待",
          "ordersTimeout": "调拨单明细加载超时,已停止等待",
          "itemsTimeout": "调拨单编辑明细加载超时,已停止等待",
          "ordersLoadFailed": "调拨单明细加载失败",
          "detailLoadFailed": "调拨单详情加载失败",
          "publishConfirm": "确定要下发调拨单「{code}」吗?",
@@ -1992,7 +2023,8 @@
          "publishSuccess": "下发执行成功",
          "publishFailed": "下发执行失败",
          "typeOptionsTimeout": "调拨类型选项加载超时,已停止等待",
          "areaOptionsTimeout": "库区选项加载超时,已停止等待"
          "areaOptionsTimeout": "库区选项加载超时,已停止等待",
          "fieldTimeout": "扩展字段加载超时,已停止等待"
        }
      },
      "transferItem": {
rsf-design/src/views/manager/task-item-log/taskItemLogPage.helpers.js
@@ -29,6 +29,9 @@
    logId: '',
    taskId: '',
    taskItemId: '',
    orderId: '',
    orderType: '',
    orderItemId: '',
    matnrId: '',
    maktx: '',
    platItemId: '',
@@ -39,6 +42,7 @@
    sourceCode: '',
    sourceId: '',
    matnrCode: '',
    trackCode: '',
    unit: '',
    anfme: '',
    batch: '',
@@ -60,7 +64,8 @@
    keeperId: '',
    keeperName: '',
    targetWarehouseId: '',
    sourceWarehouseId: ''
    sourceWarehouseId: '',
    orderBy: 'create_time desc'
  }
}
@@ -76,6 +81,7 @@
  ;[
    'condition',
    'orderType',
    'maktx',
    'platItemId',
    'platOrderCode',
@@ -83,6 +89,7 @@
    'projectCode',
    'sourceCode',
    'matnrCode',
    'trackCode',
    'unit',
    'batch',
    'spec',
@@ -109,21 +116,28 @@
      result[key] = value
    }
  })
  ;['timeStart', 'timeEnd'].forEach((key) => {
    if (params[key]) {
      result[key] = params[key]
    }
  })
  ;['logId', 'taskId', 'taskItemId', 'matnrId', 'source', 'sourceId', 'anfme', 'status'].forEach(
    (key) => {
      const value = normalizeNumber(params[key])
      if (value !== null) {
        result[key] = value
      }
  ;[
    'logId',
    'taskId',
    'taskItemId',
    'orderId',
    'orderItemId',
    'matnrId',
    'source',
    'sourceId',
    'anfme',
    'status'
  ].forEach((key) => {
    const value = normalizeNumber(params[key])
    if (value !== null) {
      result[key] = value
    }
  )
  })
  return {
    condition: '',
@@ -135,6 +149,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildTaskItemLogSearchParams(params)
  }
}
@@ -150,6 +165,9 @@
    logId: record.logId ?? '--',
    taskId: record.taskId ?? '--',
    taskItemId: record.taskItemId ?? '--',
    orderId: record.orderId ?? '--',
    orderType: normalizeReportText(record.orderType || record['orderType$']),
    orderItemId: record.orderItemId ?? '--',
    matnrId: record.matnrId ?? '--',
    maktx: normalizeReportText(record.maktx),
    platItemId: normalizeReportText(record.platItemId),
@@ -160,6 +178,7 @@
    sourceCode: normalizeReportText(record.sourceCode),
    sourceId: record.sourceId ?? '--',
    matnrCode: normalizeReportText(record.matnrCode),
    trackCode: normalizeReportText(record.trackCode),
    unit: normalizeReportText(record.unit),
    anfme: record.anfme ?? '--',
    batch: normalizeReportText(record.batch),
@@ -194,6 +213,9 @@
  return [
    { source: 'taskId', label: '任务ID' },
    { source: 'taskItemId', label: '任务明细ID' },
    { source: 'orderId', label: '单据ID' },
    { source: 'orderType', label: '单据类型' },
    { source: 'orderItemId', label: '单据明细ID' },
    { source: 'matnrId', label: '物料ID' },
    { source: 'maktx', label: '物料名称' },
    { source: 'platWorkCode', label: '工单号' },
@@ -202,6 +224,7 @@
    { source: 'source', label: '源编码' },
    { source: 'sourceCode', label: '源单号' },
    { source: 'matnrCode', label: '物料编码' },
    { source: 'trackCode', label: '追踪码' },
    { source: 'unit', label: '库存单位' },
    { source: 'anfme', label: '数量' },
    { source: 'batch', label: '库存批次' },
@@ -223,6 +246,9 @@
    return {
      taskId: row.taskId,
      taskItemId: row.taskItemId,
      orderId: row.orderId,
      orderType: row.orderType,
      orderItemId: row.orderItemId,
      matnrId: row.matnrId,
      maktx: row.maktx,
      platWorkCode: row.platWorkCode,
@@ -231,6 +257,7 @@
      source: row.source,
      sourceCode: row.sourceCode,
      matnrCode: row.matnrCode,
      trackCode: row.trackCode,
      unit: row.unit,
      anfme: row.anfme,
      batch: row.batch,
rsf-design/src/views/manager/task-item-log/taskItemLogTable.columns.js
@@ -19,6 +19,24 @@
      showOverflowTooltip: true
    },
    {
      prop: 'orderId',
      label: '单据ID',
      width: 110,
      showOverflowTooltip: true
    },
    {
      prop: 'orderType',
      label: '单据类型',
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'orderItemId',
      label: '单据明细ID',
      width: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'matnrId',
      label: '物料ID',
      width: 110,
@@ -67,6 +85,12 @@
      showOverflowTooltip: true
    },
    {
      prop: 'trackCode',
      label: '追踪码',
      minWidth: 150,
      showOverflowTooltip: true
    },
    {
      prop: 'unit',
      label: '库存单位',
      width: 100,
rsf-design/src/views/manager/task-log/index.vue
@@ -94,12 +94,42 @@
      }
    },
    {
      label: t('pages.manager.taskLog.detail.taskId'),
      key: 'taskId',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: '请输入任务ID'
      }
    },
    {
      label: t('pages.manager.taskLog.table.taskCode'),
      key: 'taskCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.manager.taskLog.search.taskCodePlaceholder')
      }
    },
    {
      label: t('pages.manager.taskLog.table.taskStatus'),
      key: 'taskStatus',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: '请输入任务状态'
      }
    },
    {
      label: t('pages.manager.taskLog.table.taskType'),
      key: 'taskType',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: '请输入任务类型'
      }
    },
    {
@@ -112,12 +142,30 @@
      }
    },
    {
      label: t('pages.manager.taskLog.table.orgSite'),
      key: 'orgSite',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入源站点'
      }
    },
    {
      label: t('pages.manager.taskLog.table.targLoc'),
      key: 'targLoc',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.manager.taskLog.search.targLocPlaceholder')
      }
    },
    {
      label: t('pages.manager.taskLog.table.targSite'),
      key: 'targSite',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入目标站点'
      }
    },
    {
@@ -139,6 +187,44 @@
      }
    },
    {
      label: t('pages.manager.taskLog.detail.exceStatus'),
      key: 'exceStatus',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: '请输入执行状态'
      }
    },
    {
      label: t('pages.manager.taskLog.detail.expDesc'),
      key: 'expDesc',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入异常描述'
      }
    },
    {
      label: t('pages.manager.taskLog.detail.sort'),
      key: 'sort',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: '请输入优先级'
      }
    },
    {
      label: t('pages.manager.taskLog.detail.expCode'),
      key: 'expCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入异常编码'
      }
    },
    {
      label: t('pages.manager.taskLog.search.timeStart'),
      key: 'timeStart',
      type: 'date',
@@ -156,6 +242,47 @@
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        type: 'date'
      }
    },
    {
      label: t('pages.manager.taskLog.table.startTime'),
      key: 'startTime',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        type: 'date'
      }
    },
    {
      label: t('pages.manager.taskLog.table.endTime'),
      key: 'endTime',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        type: 'date'
      }
    },
    {
      label: t('table.memo'),
      key: 'memo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入备注'
      }
    },
    {
      label: t('table.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: [
          { label: t('common.status.enabled'), value: 1 },
          { label: t('common.status.disabled'), value: 0 }
        ]
      }
    }
  ])
@@ -185,7 +312,8 @@
      columnsFactory: () => createTaskLogTableColumns({ handleView: openDetail })
    },
    transform: {
      dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeTaskLogRow(item)) : [])
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeTaskLogRow(item)) : []
    }
  })
@@ -241,6 +369,7 @@
  }
  function handleSearch(params) {
    searchForm.value = { ...searchForm.value, ...params }
    replaceSearchParams(buildTaskLogSearchParams(params))
    getData()
  }
rsf-design/src/views/manager/task-log/modules/task-item-log-panel.vue
New file
@@ -0,0 +1,339 @@
<template>
  <div class="task-item-log-panel space-y-4">
    <ArtSearchBar
      v-model="searchForm"
      :items="searchItems"
      :showExpand="true"
      @search="handleSearch"
      @reset="handleReset"
    />
    <div class="flex items-center justify-between">
      <div class="text-sm font-medium text-[var(--art-gray-900)]">任务明细历史档</div>
      <ListExportPrint
        class="inline-flex"
        :preview-visible="previewVisible"
        @update:previewVisible="handlePreviewVisibleChange"
        :report-title="reportTitle"
        :selected-rows="selectedRows"
        :query-params="reportQueryParams"
        :columns="reportColumns"
        :preview-rows="previewRows"
        :preview-meta="previewMeta"
        :total="pagination.total"
        :disabled="loading || !effectiveLogId"
        @export="handleExport"
        @print="handlePrint"
      />
    </div>
    <ArtTable
      :loading="loading"
      :data="data"
      :columns="columns"
      :pagination="pagination"
      @selection-change="handleSelectionChange"
      @pagination:size-change="handleSizeChange"
      @pagination:current-change="handleCurrentChange"
    />
    <TaskItemLogDetailDrawer v-model:visible="detailDrawerVisible" :detail="detailData" />
  </div>
</template>
<script setup>
  import { computed, ref, watch } from 'vue'
  import { useUserStore } from '@/store/modules/user'
  import { useTable } from '@/hooks/core/useTable'
  import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
  import ListExportPrint from '@/components/biz/list-export-print/index.vue'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import {
    fetchExportTaskItemLogReport,
    fetchGetTaskItemLogDetail,
    fetchGetTaskItemLogMany,
    fetchTaskItemLogPage
  } from '@/api/task-item-log'
  import {
    buildTaskItemLogPageQueryParams,
    buildTaskItemLogPrintRows,
    buildTaskItemLogSearchParams,
    createTaskItemLogSearchState,
    getTaskItemLogPaginationKey,
    getTaskItemLogReportColumns,
    normalizeTaskItemLogRow,
    TASK_ITEM_LOG_REPORT_TITLE
  } from '@/views/manager/task-item-log/taskItemLogPage.helpers.js'
  import { createTaskItemLogTableColumns } from '@/views/manager/task-item-log/taskItemLogTable.columns.js'
  import TaskItemLogDetailDrawer from '@/views/manager/task-item-log/modules/task-item-log-detail-drawer.vue'
  defineOptions({ name: 'TaskItemLogPanel' })
  const props = defineProps({
    logId: { type: [Number, String], default: undefined }
  })
  const userStore = useUserStore()
  const searchForm = ref(createTaskItemLogSearchState())
  const selectedRows = ref([])
  const detailDrawerVisible = ref(false)
  const detailData = ref({})
  const reportTitle = TASK_ITEM_LOG_REPORT_TITLE
  const reportColumns = getTaskItemLogReportColumns()
  const effectiveLogId = computed(() => {
    if (props.logId === '' || props.logId === null || props.logId === undefined) return undefined
    const numericValue = Number(props.logId)
    return Number.isFinite(numericValue) ? numericValue : undefined
  })
  const reportQueryParams = computed(() =>
    buildTaskItemLogSearchParams({
      ...searchForm.value,
      logId: effectiveLogId.value
    })
  )
  const searchItems = computed(() => [
    {
      label: '关键字',
      key: 'condition',
      type: 'input',
      props: { clearable: true, placeholder: '请输入物料/任务/单号关键词' }
    },
    {
      label: '任务ID',
      key: 'taskId',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入任务ID' }
    },
    {
      label: '任务明细ID',
      key: 'taskItemId',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入任务明细ID' }
    },
    {
      label: '单据ID',
      key: 'orderId',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入单据ID' }
    },
    {
      label: '单据类型',
      key: 'orderType',
      type: 'input',
      props: { clearable: true, placeholder: '请输入单据类型' }
    },
    {
      label: '物料ID',
      key: 'matnrId',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入物料ID' }
    },
    {
      label: '物料名称',
      key: 'maktx',
      type: 'input',
      props: { clearable: true, placeholder: '请输入物料名称' }
    },
    {
      label: '物料编码',
      key: 'matnrCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入物料编码' }
    },
    {
      label: '追踪码',
      key: 'trackCode',
      type: 'input',
      props: { clearable: true, placeholder: '请输入追踪码' }
    },
    {
      label: '库存单位',
      key: 'unit',
      type: 'input',
      props: { clearable: true, placeholder: '请输入库存单位' }
    },
    {
      label: '数量',
      key: 'anfme',
      type: 'inputNumber',
      props: { clearable: true, controlsPosition: 'right', placeholder: '请输入数量' }
    },
    {
      label: '库存批次',
      key: 'batch',
      type: 'input',
      props: { clearable: true, placeholder: '请输入库存批次' }
    },
    {
      label: '规格',
      key: 'spec',
      type: 'input',
      props: { clearable: true, placeholder: '请输入规格' }
    },
    {
      label: '型号',
      key: 'model',
      type: 'input',
      props: { clearable: true, placeholder: '请输入型号' }
    },
    {
      label: '字段索引',
      key: 'fieldsIndex',
      type: 'input',
      props: { clearable: true, placeholder: '请输入字段索引' }
    },
    {
      label: '备注',
      key: 'memo',
      type: 'input',
      props: { clearable: true, placeholder: '请输入备注' }
    },
    {
      label: '状态',
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        placeholder: '请选择状态',
        options: [
          { label: '正常', value: 1 },
          { label: '冻结', value: 0 }
        ]
      }
    },
    {
      label: '开始日期',
      key: 'timeStart',
      type: 'date',
      props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
    },
    {
      label: '结束日期',
      key: 'timeEnd',
      type: 'date',
      props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
    }
  ])
  function buildPanelParams(extra = {}) {
    return buildTaskItemLogPageQueryParams({
      ...searchForm.value,
      ...extra,
      logId: effectiveLogId.value
    })
  }
  function openDetail(row) {
    detailDrawerVisible.value = true
    loadDetail(row.id, row)
  }
  const {
    columns,
    data,
    loading,
    pagination,
    getData,
    replaceSearchParams,
    handleSizeChange,
    handleCurrentChange
  } = useTable({
    core: {
      apiFn: fetchTaskItemLogPage,
      apiParams: buildPanelParams(),
      paginationKey: getTaskItemLogPaginationKey(),
      columnsFactory: () => createTaskItemLogTableColumns({ handleView: openDetail })
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeTaskItemLogRow(item)) : []
    }
  })
  const resolvePrintRecords = async (payload) => {
    if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
      return defaultResponseAdapter(await fetchGetTaskItemLogMany(payload.ids)).records
    }
    return defaultResponseAdapter(
      await fetchTaskItemLogPage({
        ...reportQueryParams.value,
        logId: effectiveLogId.value,
        current: 1,
        pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : 20
      })
    ).records
  }
  const {
    previewVisible,
    previewRows,
    previewMeta,
    handlePreviewVisibleChange,
    handleExport,
    handlePrint
  } = usePrintExportPage({
    downloadFileName: 'task-item-log.xlsx',
    requestExport: (payload) =>
      fetchExportTaskItemLogReport(
        {
          ...payload,
          ...reportQueryParams.value,
          logId: effectiveLogId.value
        },
        {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }
      ),
    resolvePrintRecords,
    buildPreviewRows: (records) => buildTaskItemLogPrintRows(records),
    buildPreviewMeta: (rows) => ({
      reportTitle,
      reportDate: new Date().toLocaleDateString('zh-CN'),
      printedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
      operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
      count: rows.length
    })
  })
  async function loadDetail(id, fallback) {
    const detail = await fetchGetTaskItemLogDetail(id)
    detailData.value = normalizeTaskItemLogRow({
      ...fallback,
      ...detail
    })
  }
  function handleSelectionChange(rows) {
    selectedRows.value = Array.isArray(rows) ? rows : []
  }
  function handleSearch(params) {
    searchForm.value = { ...searchForm.value, ...params }
    replaceSearchParams(buildPanelParams(params))
    getData()
  }
  function handleReset() {
    searchForm.value = createTaskItemLogSearchState()
    selectedRows.value = []
    replaceSearchParams(buildPanelParams())
    getData()
  }
  watch(
    () => effectiveLogId.value,
    (value) => {
      if (value === undefined) return
      selectedRows.value = []
      searchForm.value = createTaskItemLogSearchState()
      replaceSearchParams(buildPanelParams())
      getData()
    },
    { immediate: true }
  )
</script>
rsf-design/src/views/manager/task-log/modules/task-log-detail-drawer.vue
@@ -8,31 +8,77 @@
    <ElScrollbar class="h-[calc(100vh-120px)]">
      <div class="flex min-h-full flex-col gap-4 pr-2">
        <ElDescriptions :column="4" border>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.taskId')">{{ detail.taskId ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.taskCode')">{{ detail.taskCode || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.taskStatus')">{{ detail.taskStatusText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.taskType')">{{ detail.taskTypeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.orgLoc')">{{ detail.orgLoc || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.orgSite')">{{ detail.orgSite || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.targLoc')">{{ detail.targLoc || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.targSite')">{{ detail.targSite || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.barcode')">{{ detail.barcode || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.robotCode')">{{ detail.robotCode || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.exceStatus')">{{ detail.exceStatusText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.sort')">{{ detail.sort ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.expDesc')">{{ detail.expDesc || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.expCode')">{{ detail.expCode || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.startTime')">{{ detail.startTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.endTime')">{{ detail.endTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.createBy')">{{ detail.createByText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.createTime')">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.updateBy')">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.updateTime')">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.taskId')">{{
            detail.taskId ?? '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.taskCode')">{{
            detail.taskCode || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.taskStatus')">{{
            detail.taskStatusText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.taskType')">{{
            detail.taskTypeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.orgLoc')">{{
            detail.orgLoc || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.orgSite')">{{
            detail.orgSite || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.targLoc')">{{
            detail.targLoc || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.targSite')">{{
            detail.targSite || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.barcode')">{{
            detail.barcode || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.robotCode')">{{
            detail.robotCode || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.exceStatus')">{{
            detail.exceStatusText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.sort')">{{
            detail.sort ?? '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.expDesc')">{{
            detail.expDesc || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.detail.expCode')">{{
            detail.expCode || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.startTime')">{{
            detail.startTimeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.manager.taskLog.table.endTime')">{{
            detail.endTimeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.createBy')">{{
            detail.createByText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.createTime')">{{
            detail.createTimeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.updateBy')">{{
            detail.updateByText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.updateTime')">{{
            detail.updateTimeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.status')">
            <ElTag :type="detail.statusType || 'info'" effect="light">{{ detail.statusText || '--' }}</ElTag>
            <ElTag :type="detail.statusType || 'info'" effect="light">{{
              detail.statusText || '--'
            }}</ElTag>
          </ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.memo')" :span="3">{{ detail.memo || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('table.memo')" :span="3">{{
            detail.memo || '--'
          }}</ElDescriptionsItem>
        </ElDescriptions>
        <TaskItemLogPanel :log-id="detail.id" />
      </div>
    </ElScrollbar>
  </ElDrawer>
@@ -40,6 +86,8 @@
<script setup>
  import { useI18n } from 'vue-i18n'
  import TaskItemLogPanel from './task-item-log-panel.vue'
  defineOptions({ name: 'TaskLogDetailDrawer' })
  defineProps({
rsf-design/src/views/manager/task-log/taskLogPage.helpers.js
@@ -39,7 +39,10 @@
    sort: '',
    expCode: '',
    startTime: '',
    endTime: ''
    endTime: '',
    memo: '',
    status: '',
    orderBy: 'create_time desc'
  }
}
@@ -63,21 +66,20 @@
    'barcode',
    'robotCode',
    'expDesc',
    'expCode'
    'expCode',
    'memo'
  ].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) {
      result[key] = value
    }
  })
  ;['timeStart', 'timeEnd', 'startTime', 'endTime'].forEach((key) => {
    if (params[key]) {
      result[key] = params[key]
    }
  })
  ;['taskId', 'taskStatus', 'taskType', 'exceStatus', 'sort'].forEach((key) => {
  ;['taskId', 'taskStatus', 'taskType', 'exceStatus', 'sort', 'status'].forEach((key) => {
    const value = normalizeNumber(params[key])
    if (value !== null) {
      result[key] = value
@@ -94,6 +96,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildTaskLogSearchParams(params)
  }
}
@@ -134,15 +137,27 @@
export function getTaskLogReportColumns() {
  return [
    { prop: 'id', label: $t('table.id') },
    { prop: 'taskId', label: $t('pages.manager.taskLog.detail.taskId') },
    { prop: 'taskCode', label: $t('pages.manager.taskLog.table.taskCode') },
    { prop: 'taskStatusText', label: $t('pages.manager.taskLog.table.taskStatus') },
    { prop: 'taskTypeText', label: $t('pages.manager.taskLog.table.taskType') },
    { prop: 'orgLoc', label: $t('pages.manager.taskLog.table.orgLoc') },
    { prop: 'orgSite', label: $t('pages.manager.taskLog.table.orgSite') },
    { prop: 'targLoc', label: $t('pages.manager.taskLog.table.targLoc') },
    { prop: 'targSite', label: $t('pages.manager.taskLog.table.targSite') },
    { prop: 'barcode', label: $t('pages.manager.taskLog.table.barcode') },
    { prop: 'robotCode', label: $t('pages.manager.taskLog.table.robotCode') },
    { prop: 'exceStatusText', label: $t('pages.manager.taskLog.detail.exceStatus') },
    { prop: 'sort', label: $t('pages.manager.taskLog.detail.sort') },
    { prop: 'expCode', label: $t('pages.manager.taskLog.detail.expCode') },
    { prop: 'startTimeText', label: $t('pages.manager.taskLog.table.startTime') },
    { prop: 'endTimeText', label: $t('pages.manager.taskLog.table.endTime') }
    { prop: 'endTimeText', label: $t('pages.manager.taskLog.table.endTime') },
    { prop: 'createByText', label: $t('table.createBy') },
    { prop: 'createTimeText', label: $t('table.createTime') },
    { prop: 'updateByText', label: $t('table.updateBy') },
    { prop: 'updateTimeText', label: $t('table.updateTime') },
    { prop: 'memo', label: $t('table.memo') }
  ]
}
@@ -150,15 +165,27 @@
  return records.map((record) => {
    const row = normalizeTaskLogRow(record)
    return {
      id: row.id ?? '--',
      taskId: row.taskId,
      taskCode: row.taskCode,
      taskStatusText: row.taskStatusText,
      taskTypeText: row.taskTypeText,
      orgLoc: row.orgLoc,
      orgSite: row.orgSite,
      targLoc: row.targLoc,
      targSite: row.targSite,
      barcode: row.barcode,
      robotCode: row.robotCode,
      exceStatusText: row.exceStatusText,
      sort: row.sort,
      expCode: row.expCode,
      startTimeText: row.startTimeText,
      endTimeText: row.endTimeText
      endTimeText: row.endTimeText,
      createByText: row.createByText,
      createTimeText: row.createTimeText,
      updateByText: row.updateByText,
      updateTimeText: row.updateTimeText,
      memo: row.memo
    }
  })
}
rsf-design/src/views/manager/task-log/taskLogTable.columns.js
@@ -8,6 +8,20 @@
    { type: 'selection', width: 48, align: 'center' },
    { type: 'globalIndex', label: $t('table.index'), width: 72, align: 'center' },
    {
      prop: 'id',
      label: $t('table.id'),
      width: 90,
      align: 'center',
      formatter: (row) => row.id ?? '--'
    },
    {
      prop: 'taskId',
      label: $t('pages.manager.taskLog.detail.taskId'),
      width: 110,
      align: 'center',
      formatter: (row) => row.taskId ?? '--'
    },
    {
      prop: 'taskCode',
      label: $t('pages.manager.taskLog.table.taskCode'),
      minWidth: 170,
@@ -62,6 +76,25 @@
      showOverflowTooltip: true
    },
    {
      prop: 'exceStatusText',
      label: $t('pages.manager.taskLog.detail.exceStatus'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'sort',
      label: $t('pages.manager.taskLog.detail.sort'),
      width: 100,
      align: 'right',
      formatter: (row) => row.sort ?? '--'
    },
    {
      prop: 'expCode',
      label: $t('pages.manager.taskLog.detail.expCode'),
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
      prop: 'startTimeText',
      label: $t('pages.manager.taskLog.table.startTime'),
      minWidth: 170,
@@ -89,12 +122,36 @@
        )
    },
    {
      prop: 'createByText',
      label: $t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'createTimeText',
      label: $t('table.createTime'),
      minWidth: 170,
      showOverflowTooltip: true
    },
    {
      prop: 'updateByText',
      label: $t('table.updateBy'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'updateTimeText',
      label: $t('table.updateTime'),
      minWidth: 170,
      showOverflowTooltip: true
    },
    {
      prop: 'memo',
      label: $t('table.memo'),
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
      prop: 'operation',
      label: $t('table.operation'),
      width: 92,
rsf-design/src/views/orders/transfer/index.vue
@@ -12,7 +12,9 @@
      <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
        <template #left>
          <ElSpace wrap>
            <ElButton v-if="canCreate" type="primary" @click="showDialog('add')" v-ripple>{{ t('pages.orders.transfer.actions.add') }}</ElButton>
            <ElButton v-if="canCreate" type="primary" @click="showDialog('add')" v-ripple>{{
              t('pages.orders.transfer.actions.add')
            }}</ElButton>
            <ElButton
              v-if="canDelete"
              type="danger"
@@ -57,6 +59,7 @@
        :transfer-data="currentTransferData"
        :type-options="typeOptions"
        :area-options="areaOptions"
        :field-definitions="fieldDefinitions"
        :submit-loading="dialogSubmitting"
        @submit="handleDialogSubmit"
      />
@@ -68,6 +71,7 @@
        :detail="detailData"
        :order-rows="detailOrderRows"
        :order-pagination="detailOrderPagination"
        :on-order-view="handleViewRelatedOrder"
        @size-change="handleDetailSizeChange"
        @current-change="handleDetailCurrentChange"
      />
@@ -90,16 +94,18 @@
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { fetchDictDataPage } from '@/api/system-manage'
  import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
  import { fetchTransferItemList } from '@/api/transfer-item'
  import {
    fetchDeleteTransfer,
    fetchEnabledTransferFields,
    fetchExportTransferReport,
    fetchTransferDetail,
    fetchTransferMany,
    fetchSaveTransferItems,
    fetchTransferOrdersPage,
    fetchTransferPage,
    fetchTransferPubOutStock,
    fetchSaveTransfer,
    fetchUpdateTransfer
    fetchUpdateTransferItems
  } from '@/api/transfer'
  import TransferDialog from './modules/transfer-dialog.vue'
  import TransferDetailDrawer from './modules/transfer-detail-drawer.vue'
@@ -107,11 +113,11 @@
  import {
    TRANSFER_REPORT_STYLE,
    buildTransferDetailOrderQueryParams,
    buildTransferDialogModel,
    buildTransferItemsSavePayload,
    buildTransferManageDialogModel,
    buildTransferPageQueryParams,
    buildTransferPrintRows,
    buildTransferReportMeta,
    buildTransferSavePayload,
    buildTransferSearchParams,
    createTransferFormState,
    createTransferSearchState,
@@ -138,6 +144,7 @@
  const searchForm = ref(createTransferSearchState())
  const typeOptions = ref([])
  const areaOptions = ref([])
  const fieldDefinitions = ref([])
  const selectedRows = ref([])
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
@@ -159,8 +166,18 @@
  const reportQueryParams = computed(() => buildTransferSearchParams(searchForm.value))
  const searchItems = computed(() => [
    { label: t('pages.orders.transfer.search.condition'), key: 'condition', type: 'input', props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.condition') } },
    { label: t('pages.orders.transfer.search.code'), key: 'code', type: 'input', props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.code') } },
    {
      label: t('pages.orders.transfer.search.condition'),
      key: 'condition',
      type: 'input',
      props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.condition') }
    },
    {
      label: t('pages.orders.transfer.search.code'),
      key: 'code',
      type: 'input',
      props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.code') }
    },
    {
      label: t('pages.orders.transfer.search.type'),
      key: 'type',
@@ -179,17 +196,94 @@
      type: 'select',
      props: { clearable: true, options: getTransferExceStatusOptions(t) }
    },
    { label: t('pages.orders.transfer.search.orgWareName'), key: 'orgWareName', type: 'input', props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.orgWareName') } },
    { label: t('pages.orders.transfer.search.tarWareName'), key: 'tarWareName', type: 'input', props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.tarWareName') } },
    { label: t('pages.orders.transfer.search.orgAreaName'), key: 'orgAreaName', type: 'input', props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.orgAreaName') } },
    { label: t('pages.orders.transfer.search.tarAreaName'), key: 'tarAreaName', type: 'input', props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.tarAreaName') } },
    {
      label: t('pages.orders.transfer.search.timeStart'),
      key: 'timeStart',
      type: 'date',
      props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' }
    },
    {
      label: t('pages.orders.transfer.search.timeEnd'),
      key: 'timeEnd',
      type: 'date',
      props: { clearable: true, type: 'date', valueFormat: 'YYYY-MM-DD' }
    },
    {
      label: t('pages.orders.transfer.search.orgWareId'),
      key: 'orgWareId',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: t('pages.orders.transfer.placeholder.orgWareId')
      }
    },
    {
      label: t('pages.orders.transfer.search.orgWareName'),
      key: 'orgWareName',
      type: 'input',
      props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.orgWareName') }
    },
    {
      label: t('pages.orders.transfer.search.tarWareId'),
      key: 'tarWareId',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: t('pages.orders.transfer.placeholder.tarWareId')
      }
    },
    {
      label: t('pages.orders.transfer.search.tarWareName'),
      key: 'tarWareName',
      type: 'input',
      props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.tarWareName') }
    },
    {
      label: t('pages.orders.transfer.search.orgAreaId'),
      key: 'orgAreaId',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: t('pages.orders.transfer.placeholder.orgAreaId')
      }
    },
    {
      label: t('pages.orders.transfer.search.orgAreaName'),
      key: 'orgAreaName',
      type: 'input',
      props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.orgAreaName') }
    },
    {
      label: t('pages.orders.transfer.search.tarAreaId'),
      key: 'tarAreaId',
      type: 'inputNumber',
      props: {
        clearable: true,
        controlsPosition: 'right',
        placeholder: t('pages.orders.transfer.placeholder.tarAreaId')
      }
    },
    {
      label: t('pages.orders.transfer.search.tarAreaName'),
      key: 'tarAreaName',
      type: 'input',
      props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.tarAreaName') }
    },
    {
      label: t('pages.orders.transfer.search.status'),
      key: 'status',
      type: 'select',
      props: { clearable: true, options: getTransferStatusOptions(t) }
    },
    { label: t('pages.orders.transfer.search.memo'), key: 'memo', type: 'input', props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.memo') } }
    {
      label: t('pages.orders.transfer.search.memo'),
      key: 'memo',
      type: 'input',
      props: { clearable: true, placeholder: t('pages.orders.transfer.placeholder.memo') }
    }
  ])
  function handleSelectionChange(rows) {
@@ -211,24 +305,32 @@
    }
  }
  async function loadTransferOrders(code) {
  async function loadTransferOrders(transferId, code) {
    detailOrdersLoading.value = true
    try {
      const response = await guardRequestWithMessage(
        fetchTransferOrdersPage(
          buildTransferDetailOrderQueryParams({
            id: transferId,
            code,
            current: detailOrderPagination.current,
            pageSize: detailOrderPagination.size
          })
        ),
        { records: [], total: 0, current: detailOrderPagination.current, size: detailOrderPagination.size },
        {
          records: [],
          total: 0,
          current: detailOrderPagination.current,
          size: detailOrderPagination.size
        },
        { timeoutMessage: t('pages.orders.transfer.messages.ordersTimeout') }
      )
      const normalized = defaultResponseAdapter(response)
      detailOrderRows.value = normalized.records.map((item) => normalizeTransferOrderRow(item, t))
      detailOrderPagination.total = Number(normalized.total || 0)
      detailOrderPagination.current = Number(normalized.current || detailOrderPagination.current || 1)
      detailOrderPagination.current = Number(
        normalized.current || detailOrderPagination.current || 1
      )
      detailOrderPagination.size = Number(normalized.size || detailOrderPagination.size || 20)
    } catch (error) {
      detailOrderRows.value = []
@@ -249,7 +351,7 @@
    try {
      await loadTransferDetail(row.id)
      activeTransferCode.value = detailData.value.code || row.code || activeTransferCode.value
      await loadTransferOrders(activeTransferCode.value)
      await loadTransferOrders(row.id, activeTransferCode.value)
    } catch (error) {
      detailDrawerVisible.value = false
      detailData.value = {}
@@ -260,12 +362,23 @@
  async function openEditDialog(row) {
    try {
      const detail = await guardRequestWithMessage(
        fetchTransferDetail(row.id),
        {},
        { timeoutMessage: t('pages.orders.transfer.messages.detailTimeout') }
      )
      showDialog('edit', detail)
      const [detail, itemResponse] = await Promise.all([
        guardRequestWithMessage(
          fetchTransferDetail(row.id),
          {},
          { timeoutMessage: t('pages.orders.transfer.messages.detailTimeout') }
        ),
        guardRequestWithMessage(fetchTransferItemList({ transferId: row.id }), [], {
          timeoutMessage: t('pages.orders.transfer.messages.itemsTimeout')
        })
      ])
      const itemRecords = Array.isArray(itemResponse)
        ? itemResponse
        : defaultResponseAdapter(itemResponse).records
      showDialog('edit', {
        transfer: detail,
        items: itemRecords
      })
    } catch (error) {
      ElMessage.error(error?.message || t('pages.orders.transfer.messages.detailLoadFailed'))
    }
@@ -273,21 +386,23 @@
  async function handlePublish(row) {
    try {
      await ElMessageBox.confirm(t('pages.orders.transfer.messages.publishConfirm', { code: row.code || row.id }), t('pages.orders.transfer.messages.publishTitle'), {
        confirmButtonText: t('common.confirm'),
        cancelButtonText: t('common.cancel'),
        type: 'warning'
      })
      const response = await fetchTransferPubOutStock({ id: row.id })
      if (response?.code !== 200 && response?.success !== true) {
        throw new Error(response?.message || t('pages.orders.transfer.messages.publishFailed'))
      }
      ElMessage.success(response?.message || t('pages.orders.transfer.messages.publishSuccess'))
      await ElMessageBox.confirm(
        t('pages.orders.transfer.messages.publishConfirm', { code: row.code || row.id }),
        t('pages.orders.transfer.messages.publishTitle'),
        {
          confirmButtonText: t('common.confirm'),
          cancelButtonText: t('common.cancel'),
          type: 'warning'
        }
      )
      await fetchTransferPubOutStock({ id: row.id })
      ElMessage.success(t('pages.orders.transfer.messages.publishSuccess'))
      await refreshData()
      if (detailDrawerVisible.value && activeTransferId.value === row.id) {
        await loadTransferDetail(row.id)
        await loadTransferOrders(row.code || activeTransferCode.value)
        await loadTransferOrders(row.id, row.code || activeTransferCode.value)
      }
      router.push('/orders/out-stock')
    } catch (error) {
      if (error === 'cancel' || error?.message === 'cancel') return
      ElMessage.error(error?.message || t('pages.orders.transfer.messages.publishFailed'))
@@ -346,10 +461,15 @@
      apiFn: fetchTransferPage,
      apiParams: buildTransferPageQueryParams(searchForm.value),
      paginationKey: getTransferPaginationKey(),
      columnsFactory: () => createTransferTableColumns({ handleActionClick })
      columnsFactory: () =>
        createTransferTableColumns({
          handleActionClick,
          handleViewOrder: handleViewRelatedOrder
        })
    },
    transform: {
      dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeTransferRow(item, t)) : [])
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeTransferRow(item, t)) : []
    }
  })
@@ -357,18 +477,21 @@
    dialogVisible,
    dialogType,
    currentRecord: currentTransferData,
    selectedRows: crudSelectedRows,
    handleSelectionChange: handleCrudSelectionChange,
    showDialog,
    handleDialogSubmit: handleCrudDialogSubmit,
    handleDelete,
    handleBatchDelete
  } = useCrudPage({
    createEmptyModel: () => createTransferFormState(),
    buildEditModel: (record) => buildTransferDialogModel(record),
    buildSavePayload: (formData) => buildTransferSavePayload(formData, areaOptions.value),
    saveRequest: fetchSaveTransfer,
    updateRequest: fetchUpdateTransfer,
    createEmptyModel: () => ({
      transfer: createTransferFormState(),
      items: []
    }),
    buildEditModel: (record) => buildTransferManageDialogModel(record, fieldDefinitions.value),
    buildSavePayload: (formData) =>
      buildTransferItemsSavePayload(formData, areaOptions.value, fieldDefinitions.value),
    saveRequest: fetchSaveTransferItems,
    updateRequest: fetchUpdateTransferItems,
    deleteRequest: fetchDeleteTransfer,
    entityName: t('pages.orders.transfer.entity'),
    resolveRecordLabel: (record) => record?.code || record?.id,
@@ -387,7 +510,12 @@
  async function loadTypeOptions() {
    const response = await guardRequestWithMessage(
      fetchDictDataPage({ current: 1, pageSize: 200, dictTypeCode: 'sys_transfer_type', status: 1 }),
      fetchDictDataPage({
        current: 1,
        pageSize: 200,
        dictTypeCode: 'sys_transfer_type',
        status: 1
      }),
      { records: [] },
      { timeoutMessage: t('pages.orders.transfer.messages.typeOptionsTimeout') }
    )
@@ -403,6 +531,23 @@
    areaOptions.value = resolveTransferAreaOptions(defaultResponseAdapter(response).records)
  }
  async function loadFieldDefinitions() {
    const records = await guardRequestWithMessage(fetchEnabledTransferFields(), [], {
      timeoutMessage: t('pages.orders.transfer.messages.fieldTimeout')
    })
    fieldDefinitions.value = Array.isArray(records) ? records : []
  }
  function handleViewRelatedOrder(row) {
    if (row?.type === 'out') {
      router.push('/orders/out-stock')
      return
    }
    if (row?.type === 'in') {
      router.push('/orders/asn-order')
    }
  }
  function handleSearch(params) {
    searchForm.value = { ...searchForm.value, ...params }
    replaceSearchParams(buildTransferSearchParams(searchForm.value))
@@ -416,13 +561,13 @@
  async function handleDetailCurrentChange(current) {
    detailOrderPagination.current = current
    await loadTransferOrders(activeTransferCode.value)
    await loadTransferOrders(activeTransferId.value, activeTransferCode.value)
  }
  async function handleDetailSizeChange(size) {
    detailOrderPagination.size = size
    detailOrderPagination.current = 1
    await loadTransferOrders(activeTransferCode.value)
    await loadTransferOrders(activeTransferId.value, activeTransferCode.value)
  }
  const resolvePrintRecords = async (payload) => {
@@ -433,7 +578,8 @@
      await fetchTransferPage({
        ...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
  }
@@ -448,11 +594,14 @@
  } = usePrintExportPage({
    downloadFileName: 'transfer.xlsx',
    requestExport: (payload) =>
      fetchExportTransferReport(Array.isArray(payload?.ids) && payload.ids.length > 0 ? reportQueryParams.value : payload, {
        headers: {
          Authorization: userStore.accessToken || ''
      fetchExportTransferReport(
        Array.isArray(payload?.ids) && payload.ids.length > 0 ? reportQueryParams.value : payload,
        {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }
      }),
      ),
    resolvePrintRecords,
    buildPreviewRows: (records) => buildTransferPrintRows(records, t),
    buildPreviewMeta: (rows) => {
@@ -478,12 +627,13 @@
    buildTransferReportMeta({
      previewMeta: rawPreviewMeta.value,
      count: previewRows.value.length,
      orientation: rawPreviewMeta.value?.reportStyle?.orientation || TRANSFER_REPORT_STYLE.orientation,
      orientation:
        rawPreviewMeta.value?.reportStyle?.orientation || TRANSFER_REPORT_STYLE.orientation,
      t
    })
  )
  onMounted(async () => {
    await Promise.allSettled([loadTypeOptions(), loadAreaOptions()])
    await Promise.allSettled([loadTypeOptions(), loadAreaOptions(), loadFieldDefinitions()])
  })
</script>
rsf-design/src/views/orders/transfer/modules/transfer-detail-drawer.vue
@@ -12,8 +12,12 @@
      </div>
      <div v-else class="space-y-4">
        <ElDescriptions :title="t('pages.orders.transfer.detail.baseInfo')" :column="2" border>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.code')">{{ detail.code || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.type')">{{ detail.typeLabel || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.code')">{{
            detail.code || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.type')">{{
            detail.typeLabel || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.source')">
            <ElTag :type="detail.sourceTagType || 'info'" effect="light">
              {{ detail.sourceText || '--' }}
@@ -24,28 +28,48 @@
              {{ detail.exceStatusText || '--' }}
            </ElTag>
          </ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.orgWareName')">{{ detail.orgWareName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.tarWareName')">{{ detail.tarWareName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.orgAreaName')">{{ detail.orgAreaName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.tarAreaName')">{{ detail.tarAreaName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.orgWareName')">{{
            detail.orgWareName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.tarWareName')">{{
            detail.tarWareName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.orgAreaName')">{{
            detail.orgAreaName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.tarAreaName')">{{
            detail.tarAreaName || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.status')">
            <ElTag :type="detail.statusType || 'info'" effect="light">
              {{ detail.statusText || '--' }}
            </ElTag>
          </ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.memo')" :span="2">{{ detail.memo || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.memo')" :span="2">{{
            detail.memo || '--'
          }}</ElDescriptionsItem>
        </ElDescriptions>
        <ElDescriptions :title="t('pages.orders.transfer.detail.auditInfo')" :column="2" border>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.createBy')">{{ detail.createByText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.createTime')">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.updateBy')">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.updateTime')">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.createBy')">{{
            detail.createByText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.createTime')">{{
            detail.createTimeText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.updateBy')">{{
            detail.updateByText || '--'
          }}</ElDescriptionsItem>
          <ElDescriptionsItem :label="t('pages.orders.transfer.detail.updateTime')">{{
            detail.updateTimeText || '--'
          }}</ElDescriptionsItem>
        </ElDescriptions>
        <div class="space-y-3">
          <div class="flex items-center justify-between">
            <div class="text-sm font-medium text-[var(--art-gray-900)]">{{ t('pages.orders.transfer.detail.relatedOrders') }}</div>
            <div class="text-sm font-medium text-[var(--art-gray-900)]">{{
              t('pages.orders.transfer.detail.relatedOrders')
            }}</div>
            <ElTag effect="plain">{{ t('common.count', { count: orderRows.length }) }}</ElTag>
          </div>
          <ArtTable
@@ -76,13 +100,18 @@
    ordersLoading: { type: Boolean, default: false },
    detail: { type: Object, default: () => ({}) },
    orderRows: { type: Array, default: () => [] },
    orderPagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
    orderPagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) },
    onOrderView: { type: Function, default: null }
  })
  const emit = defineEmits(['update:visible', 'size-change', 'current-change'])
  const { t } = useI18n()
  const orderColumns = createTransferOrderTableColumns()
  const orderColumns = computed(() =>
    createTransferOrderTableColumns({
      handleViewOrder: props.onOrderView
    })
  )
  const visible = computed({
    get: () => props.visible,
rsf-design/src/views/orders/transfer/modules/transfer-dialog.vue
@@ -1,36 +1,226 @@
<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="handleOpenMaterialDialog">
            {{ t('pages.orders.transfer.dialog.addMaterial') }}
          </ElButton>
          <ElButton
            type="danger"
            plain
            :disabled="isEdit || selectedItemKeys.length === 0"
            @click="handleBatchRemove"
          >
            {{ t('pages.orders.transfer.dialog.deleteSelected') }}
          </ElButton>
        </ElSpace>
        <div class="text-xs text-[var(--art-gray-600)]">
          {{ t('pages.orders.transfer.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
          prop="spec"
          :label="t('pages.orders.transferItem.table.spec')"
          min-width="140"
          show-overflow-tooltip
        />
        <ElTableColumn
          prop="model"
          :label="t('pages.orders.transferItem.table.model')"
          min-width="140"
          show-overflow-tooltip
        />
        <ElTableColumn :label="t('table.quantity')" width="150" 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.transfer.dialog.supplierCode')"
          min-width="150"
          show-overflow-tooltip
        >
          <template #default="{ row }">
            <ElInput v-model="row.splrCode" clearable />
          </template>
        </ElTableColumn>
        <ElTableColumn
          :label="t('pages.orders.transfer.dialog.supplierName')"
          min-width="180"
          show-overflow-tooltip
        >
          <template #default="{ row }">
            <ElInput v-model="row.splrName" clearable />
          </template>
        </ElTableColumn>
        <ElTableColumn :label="t('table.batch')" min-width="140" show-overflow-tooltip>
          <template #default="{ row }">
            <ElInput v-model="row.batch" clearable />
          </template>
        </ElTableColumn>
        <ElTableColumn :label="t('table.unit')" width="120" align="center">
          <template #default="{ row }">
            <ElInput v-model="row.unit" clearable />
          </template>
        </ElTableColumn>
        <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" :disabled="isEdit" @click="handleRemoveRow(row)">
              {{ t('pages.orders.transfer.actions.delete') }}
            </ElButton>
          </template>
        </ElTableColumn>
      </ElTable>
    </div>
    <template #footer>
      <ElSpace>
        <ElButton @click="handleVisibleChange(false)">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" :loading="submitLoading" @click="handleSubmit">
          {{ t('common.confirm') }}
        </ElButton>
      </ElSpace>
    </template>
    <TransferMaterialDialog
      v-model:visible="materialDialogVisible"
      :org-area-id="form.orgAreaId"
      :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 {
    buildTransferDialogModel,
    createTransferFormState,
    getTransferStatusOptions
    buildTransferManageDialogModel,
    createTransferEditableItemFromMaterial,
    createTransferFormState
  } from '../transferPage.helpers.js'
  import TransferMaterialDialog from './transfer-material-dialog.vue'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    dialogType: { type: String, default: 'add' },
    transferData: { type: Object, default: () => ({}) },
    transferData: {
      type: Object,
      default: () => ({
        transfer: {},
        items: []
      })
    },
    typeOptions: { type: Array, default: () => [] },
    areaOptions: { type: Array, default: () => [] },
    fieldDefinitions: { type: Array, default: () => [] },
    submitLoading: { type: Boolean, default: false }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const { t } = useI18n()
  const formRef = ref()
  const form = reactive(createTransferFormState())
  const { t } = useI18n()
  const itemRows = ref([])
  const selectedItemKeys = ref([])
  const materialDialogVisible = ref(false)
  const isEdit = computed(() => props.dialogType === 'edit')
  const dialogTitle = computed(() =>
    isEdit.value ? t('pages.orders.transfer.dialog.titleEdit') : t('pages.orders.transfer.dialog.titleAdd')
    isEdit.value
      ? t('pages.orders.transfer.dialog.titleEdit')
      : t('pages.orders.transfer.dialog.titleAdd')
  )
  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.transfer.dialog.validation.type'), trigger: 'change' }],
    orgAreaId: [{ required: true, message: t('pages.orders.transfer.dialog.validation.orgAreaId'), trigger: 'change' }],
    tarAreaId: [{ required: true, message: t('pages.orders.transfer.dialog.validation.tarAreaId'), trigger: 'change' }]
    type: [
      {
        required: true,
        message: t('pages.orders.transfer.dialog.validation.type'),
        trigger: 'change'
      }
    ],
    orgAreaId: [
      {
        required: true,
        message: t('pages.orders.transfer.dialog.validation.orgAreaId'),
        trigger: 'change'
      }
    ],
    tarAreaId: [
      {
        required: true,
        message: t('pages.orders.transfer.dialog.validation.tarAreaId'),
        trigger: 'change'
      }
    ]
  }))
  const formItems = computed(() => [
@@ -84,7 +274,10 @@
      props: {
        placeholder: t('pages.orders.transfer.dialog.placeholderStatus'),
        clearable: true,
        options: getTransferStatusOptions()
        options: [
          { label: t('pages.orders.transfer.status.normal'), value: 1 },
          { label: t('pages.orders.transfer.status.frozen'), value: 0 }
        ]
      }
    },
    {
@@ -101,38 +294,110 @@
    }
  ])
  const loadFormData = () => {
    Object.assign(form, buildTransferDialogModel(props.transferData))
  function loadDialogData() {
    const dialogData = buildTransferManageDialogModel(props.transferData, props.fieldDefinitions)
    Object.assign(form, dialogData.transfer)
    itemRows.value = dialogData.items
    selectedItemKeys.value = []
  }
  const resetForm = () => {
  function resetDialogData() {
    Object.assign(form, createTransferFormState())
    itemRows.value = []
    selectedItemKeys.value = []
    materialDialogVisible.value = false
    formRef.value?.clearValidate?.()
  }
  const handleSubmit = async () => {
  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 handleOpenMaterialDialog() {
    if (!form.orgAreaId) {
      ElMessage.warning(t('pages.orders.transfer.dialog.validation.orgAreaId'))
      return
    }
    if (!form.tarAreaId) {
      ElMessage.warning(t('pages.orders.transfer.dialog.validation.tarAreaId'))
      return
    }
    materialDialogVisible.value = true
  }
  function handleMaterialConfirm(rows) {
    const existingMatnrIds = new Set(selectedMatnrIds.value)
    const nextRows = rows
      .map((item) => createTransferEditableItemFromMaterial(item, props.fieldDefinitions))
      .filter((item) => !existingMatnrIds.has(item.matnrId))
    if (!nextRows.length) {
      ElMessage.warning(t('pages.orders.transfer.dialog.materialDuplicate'))
      return
    }
    itemRows.value = [...itemRows.value, ...nextRows]
  }
  function validateItems() {
    if (!itemRows.value.length) {
      ElMessage.warning(t('pages.orders.transfer.dialog.validation.items'))
      return false
    }
    const invalidRow = itemRows.value.find((item) => Number(item.anfme) <= 0)
    if (invalidRow) {
      ElMessage.warning(t('pages.orders.transfer.dialog.validation.anfme'))
      return false
    }
    return true
  }
  async function handleSubmit() {
    if (!formRef.value) return
    try {
      await formRef.value.validate()
      emit('submit', { ...form })
    } catch {
      return
    }
    if (!validateItems()) {
      return
    }
    emit('submit', {
      transfer: { ...form },
      items: itemRows.value.map((item) => ({ ...item }))
    })
  }
  const handleCancel = () => {
    emit('update:visible', false)
  function handleVisibleChange(visible) {
    emit('update:visible', visible)
  }
  const handleClosed = () => {
    resetForm()
  function handleClosed() {
    resetDialogData()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (visible) {
        loadFormData()
        loadDialogData()
        nextTick(() => {
          formRef.value?.clearValidate?.()
        })
@@ -145,44 +410,9 @@
    () => props.transferData,
    () => {
      if (props.visible) {
        loadFormData()
        loadDialogData()
      }
    },
    { deep: true }
  )
</script>
<template>
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    width="760px"
    align-center
    destroy-on-close
    @update:model-value="handleCancel"
    @closed="handleClosed"
  >
    <div class="mb-3 rounded-lg border border-[var(--art-border-color)] bg-[var(--art-bg-color)] px-3 py-2 text-xs text-[var(--art-text-gray-600)]">
      {{ t('pages.orders.transfer.dialog.tip') }}
    </div>
    <ArtForm
      ref="formRef"
      v-model="form"
      :items="formItems"
      :rules="rules"
      :span="12"
      :gutter="20"
      label-width="110px"
      :show-reset="false"
      :show-submit="false"
    />
    <template #footer>
      <span class="dialog-footer">
        <ElButton @click="handleCancel">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" :loading="submitLoading" @click="handleSubmit">{{ t('common.confirm') }}</ElButton>
      </span>
    </template>
  </ElDialog>
</template>
rsf-design/src/views/orders/transfer/modules/transfer-material-dialog.vue
New file
@@ -0,0 +1,252 @@
<template>
  <ElDialog
    :model-value="visible"
    :title="t('pages.orders.transfer.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 { fetchTransferLocsItemsPage } from '@/api/transfer'
  import {
    createTransferEditableItemFromMaterial,
    createTransferMaterialSearchState
  } from '../transferPage.helpers'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    orgAreaId: { type: [Number, String], default: undefined },
    fieldDefinitions: { type: Array, default: () => [] },
    selectedMatnrIds: { type: Array, default: () => [] }
  })
  const emit = defineEmits(['update:visible', 'confirm'])
  const { t } = useI18n()
  const searchForm = ref(createTransferMaterialSearchState())
  const selectedRows = ref([])
  const searchItems = computed(() => [
    {
      label: t('table.materialName'),
      key: 'maktx',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.transfer.materialDialog.placeholder.maktx')
      }
    },
    {
      label: t('table.materialCode'),
      key: 'matnrCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.orders.transfer.materialDialog.placeholder.matnrCode')
      }
    }
  ])
  const createColumns = () => {
    const baseColumns = [
      { type: 'selection', width: 48, align: 'center' },
      { type: 'globalIndex', label: t('table.index'), width: 72, align: 'center' },
      {
        prop: 'matnrCode',
        label: t('table.materialCode'),
        minWidth: 150,
        showOverflowTooltip: true
      },
      {
        prop: 'maktx',
        label: t('table.materialName'),
        minWidth: 220,
        showOverflowTooltip: true
      },
      {
        prop: 'spec',
        label: t('pages.orders.transferItem.table.spec'),
        minWidth: 140,
        showOverflowTooltip: true
      },
      {
        prop: 'model',
        label: t('pages.orders.transferItem.table.model'),
        minWidth: 140,
        showOverflowTooltip: true
      },
      {
        prop: 'batch',
        label: t('table.batch'),
        minWidth: 140,
        showOverflowTooltip: true
      },
      {
        prop: 'unit',
        label: t('table.unit'),
        width: 100,
        align: 'center'
      },
      {
        prop: 'anfme',
        label: t('table.quantity'),
        width: 110,
        align: 'right'
      },
      {
        prop: 'selected',
        label: t('pages.orders.transfer.materialDialog.table.status'),
        width: 110,
        formatter: (row) =>
          h(
            ElTag,
            {
              type: props.selectedMatnrIds.includes(row.matnrId) ? 'warning' : 'info',
              effect: 'light'
            },
            () =>
              props.selectedMatnrIds.includes(row.matnrId)
                ? t('pages.orders.transfer.materialDialog.selected')
                : t('pages.orders.transfer.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: fetchTransferLocsItemsPage,
      apiParams: {
        ...createTransferMaterialSearchState(),
        orgAreaId: props.orgAreaId
      },
      immediate: false,
      columnsFactory: createColumns
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records)
          ? records.map((item) => {
              const editable = createTransferEditableItemFromMaterial(item, props.fieldDefinitions)
              return {
                ...item,
                ...editable
              }
            })
          : []
    }
  })
  function buildParams() {
    return {
      ...searchForm.value,
      orgAreaId: props.orgAreaId
    }
  }
  function handleSelectionChange(rows) {
    selectedRows.value = Array.isArray(rows) ? rows : []
  }
  function handleSearch(params) {
    searchForm.value = { ...searchForm.value, ...params }
    replaceSearchParams(buildParams())
    getData()
  }
  function handleReset() {
    searchForm.value = createTransferMaterialSearchState()
    replaceSearchParams(buildParams())
    getData()
  }
  function handleConfirm() {
    emit(
      'confirm',
      selectedRows.value.map((item) => ({ ...item }))
    )
    handleVisibleChange(false)
  }
  function handleVisibleChange(visible) {
    emit('update:visible', visible)
  }
  watch(
    () => props.visible,
    (visible) => {
      if (!visible) {
        searchForm.value = createTransferMaterialSearchState()
        selectedRows.value = []
        return
      }
      searchForm.value = createTransferMaterialSearchState()
      replaceSearchParams(buildParams())
      getData()
    }
  )
  watch(
    () => props.fieldDefinitions,
    () => {
      resetColumns?.()
    },
    { deep: true }
  )
</script>
rsf-design/src/views/orders/transfer/modules/transfer-orders-panel.vue
New file
@@ -0,0 +1,76 @@
<template>
  <div class="transfer-orders-panel">
    <ArtTable :loading="loading" :data="rows" :columns="columns" />
  </div>
</template>
<script setup>
  import { computed, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import { fetchTransferOrdersPage } from '@/api/transfer'
  import {
    buildTransferDetailOrderQueryParams,
    normalizeTransferOrderRow
  } from '../transferPage.helpers.js'
  import { createTransferOrderTableColumns } from '../transferTable.columns.js'
  defineOptions({ name: 'TransferOrdersPanel' })
  const props = defineProps({
    transferId: { type: [Number, String], default: undefined },
    transferCode: { type: String, default: '' },
    onOrderView: { type: Function, default: null }
  })
  const loading = ref(false)
  const rows = ref([])
  const { t } = useI18n()
  const columns = computed(() =>
    createTransferOrderTableColumns({
      handleViewOrder: props.onOrderView
    })
  )
  async function loadRows() {
    if (props.transferId === undefined || props.transferId === null || props.transferId === '') {
      rows.value = []
      return
    }
    loading.value = true
    try {
      const response = await fetchTransferOrdersPage(
        buildTransferDetailOrderQueryParams({
          id: props.transferId,
          code: props.transferCode,
          current: 1,
          pageSize: 200
        })
      )
      const records = defaultResponseAdapter(response).records
      rows.value = Array.isArray(records)
        ? records.map((item) => normalizeTransferOrderRow(item, t))
        : []
    } catch {
      rows.value = []
    } finally {
      loading.value = false
    }
  }
  watch(
    () => [props.transferId, props.transferCode],
    () => {
      loadRows()
    },
    { immediate: true }
  )
</script>
<style scoped>
  .transfer-orders-panel {
    padding: 12px 0;
  }
</style>
rsf-design/src/views/orders/transfer/transferPage.helpers.js
@@ -53,18 +53,23 @@
export function createTransferSearchState() {
  return {
    condition: '',
    timeStart: '',
    timeEnd: '',
    code: '',
    type: '',
    source: '',
    exceStatus: '',
    orgWareId: '',
    orgWareName: '',
    tarWareId: '',
    tarWareName: '',
    orgAreaId: '',
    orgAreaName: '',
    tarAreaId: '',
    tarAreaName: '',
    memo: '',
    status: '',
    timeStart: '',
    timeEnd: ''
    orderBy: 'create_time desc'
  }
}
@@ -85,10 +90,18 @@
export function buildTransferDialogModel(record = {}) {
  return {
    ...createTransferFormState(),
    ...(record.id !== undefined && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
    ...(record.id !== undefined && record.id !== null && record.id !== ''
      ? { id: Number(record.id) }
      : {}),
    code: normalizeText(record.code || ''),
    type: record.type !== undefined && record.type !== null && record.type !== '' ? Number(record.type) : '',
    source: record.source !== undefined && record.source !== null && record.source !== '' ? Number(record.source) : 2,
    type:
      record.type !== undefined && record.type !== null && record.type !== ''
        ? Number(record.type)
        : '',
    source:
      record.source !== undefined && record.source !== null && record.source !== ''
        ? Number(record.source)
        : 2,
    exceStatus:
      record.exceStatus !== undefined && record.exceStatus !== null && record.exceStatus !== ''
        ? Number(record.exceStatus)
@@ -103,6 +116,35 @@
        : void 0,
    status: record.status !== undefined && record.status !== null ? Number(record.status) : 1,
    memo: normalizeText(record.memo || '')
  }
}
function createTransferItemRowKey(record = {}) {
  const keySeed = [
    record.id ?? 'new',
    record.transferId ?? 'transfer',
    record.matnrId ?? 'matnr',
    record.batch ?? '',
    record.fieldsIndex ?? ''
  ]
  return keySeed.join('-')
}
function resolveDynamicFieldValues(record = {}, fieldDefinitions = []) {
  return Object.fromEntries(
    (Array.isArray(fieldDefinitions) ? fieldDefinitions : []).map((item) => [
      item.fields,
      record[item.fields] ?? record.extendFields?.[item.fields] ?? ''
    ])
  )
}
export function buildTransferManageDialogModel(record = {}, fieldDefinitions = []) {
  const transferRecord = record?.transfer || record || {}
  const itemRecords = Array.isArray(record?.items) ? record.items : []
  return {
    transfer: buildTransferDialogModel(transferRecord),
    items: itemRecords.map((item) => buildTransferEditableItem(item, fieldDefinitions))
  }
}
@@ -136,11 +178,28 @@
export function buildTransferSearchParams(params = {}) {
  const result = {}
  ;['condition', 'code', 'orgWareName', 'tarWareName', 'orgAreaName', 'tarAreaName', 'memo'].forEach((key) => {
  ;[
    'condition',
    'code',
    'orgWareName',
    'tarWareName',
    'orgAreaName',
    'tarAreaName',
    'memo'
  ].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) result[key] = value
  })
  ;['type', 'source', 'exceStatus', 'status', 'orgWareId', 'tarWareId', 'orgAreaId', 'tarAreaId'].forEach((key) => {
  ;[
    'type',
    'source',
    'exceStatus',
    'status',
    'orgWareId',
    'tarWareId',
    'orgAreaId',
    'tarAreaId'
  ].forEach((key) => {
    if (params[key] !== '' && params[key] !== undefined && params[key] !== null) {
      result[key] = normalizeNumber(params[key])
    }
@@ -158,16 +217,21 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildTransferSearchParams(params)
  }
}
export function buildTransferDetailOrderQueryParams(params = {}) {
  return {
    ...(normalizeNumber(params.id, void 0) !== void 0
      ? { id: normalizeNumber(params.id, void 0) }
      : {}),
    condition: normalizeText(params.code || params.condition),
    code: normalizeText(params.code || params.condition),
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc'
  }
}
@@ -186,8 +250,14 @@
  const tarAreaId = normalizeNumber(formData.tarAreaId, void 0)
  const orgArea = optionMap.get(orgAreaId) || {}
  const tarArea = optionMap.get(tarAreaId) || {}
  const orgWareId = normalizeNumber(orgArea.warehouseId ?? orgArea.warehouse_id ?? orgArea.warehouseIdValue, void 0)
  const tarWareId = normalizeNumber(tarArea.warehouseId ?? tarArea.warehouse_id ?? tarArea.warehouseIdValue, void 0)
  const orgWareId = normalizeNumber(
    orgArea.warehouseId ?? orgArea.warehouse_id ?? orgArea.warehouseIdValue,
    void 0
  )
  const tarWareId = normalizeNumber(
    tarArea.warehouseId ?? tarArea.warehouse_id ?? tarArea.warehouseIdValue,
    void 0
  )
  return {
    ...(formData.id !== undefined && formData.id !== null && formData.id !== ''
@@ -200,17 +270,27 @@
    ...(formData.source !== undefined && formData.source !== null && formData.source !== ''
      ? { source: normalizeNumber(formData.source) }
      : {}),
    ...(formData.exceStatus !== undefined && formData.exceStatus !== null && formData.exceStatus !== ''
    ...(formData.exceStatus !== undefined &&
    formData.exceStatus !== null &&
    formData.exceStatus !== ''
      ? { exceStatus: normalizeNumber(formData.exceStatus) }
      : {}),
    ...(orgAreaId !== void 0 ? { orgAreaId } : {}),
    ...(tarAreaId !== void 0 ? { tarAreaId } : {}),
    ...(orgWareId !== void 0 ? { orgWareId } : {}),
    ...(tarWareId !== void 0 ? { tarWareId } : {}),
    ...(normalizeText(orgArea.name || orgArea.areaName) ? { orgAreaName: normalizeText(orgArea.name || orgArea.areaName) } : {}),
    ...(normalizeText(tarArea.name || tarArea.areaName) ? { tarAreaName: normalizeText(tarArea.name || tarArea.areaName) } : {}),
    ...(normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) ? { orgWareName: normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) } : {}),
    ...(normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) ? { tarWareName: normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) } : {}),
    ...(normalizeText(orgArea.name || orgArea.areaName)
      ? { orgAreaName: normalizeText(orgArea.name || orgArea.areaName) }
      : {}),
    ...(normalizeText(tarArea.name || tarArea.areaName)
      ? { tarAreaName: normalizeText(tarArea.name || tarArea.areaName) }
      : {}),
    ...(normalizeText(orgArea.warehouseId$ || orgArea.warehouseName)
      ? { orgWareName: normalizeText(orgArea.warehouseId$ || orgArea.warehouseName) }
      : {}),
    ...(normalizeText(tarArea.warehouseId$ || tarArea.warehouseName)
      ? { tarWareName: normalizeText(tarArea.warehouseId$ || tarArea.warehouseName) }
      : {}),
    ...(formData.status !== undefined && formData.status !== null && formData.status !== ''
      ? { status: normalizeNumber(formData.status) }
      : { status: 1 }),
@@ -219,16 +299,34 @@
}
function resolveAreaText(record = {}, key) {
  return normalizeText(record[`${key}AreaName$`] || record[`${key}AreaName`] || record[`${key}AreaId$`] || record[`${key}AreaId`])
  return normalizeText(
    record[`${key}AreaName$`] ||
      record[`${key}AreaName`] ||
      record[`${key}AreaId$`] ||
      record[`${key}AreaId`]
  )
}
function resolveWarehouseText(record = {}, key) {
  return normalizeText(record[`${key}WareName$`] || record[`${key}WareName`] || record[`${key}WareId$`] || record[`${key}WareId`])
  return normalizeText(
    record[`${key}WareName$`] ||
      record[`${key}WareName`] ||
      record[`${key}WareId$`] ||
      record[`${key}WareId`]
  )
}
export function normalizeTransferRow(record = {}, t = $t) {
  const statusMeta = metaByValue(record.statusBool ?? record.status, getTransferStatusMetaMap(t), t('common.status.unknown'))
  const exceStatusMeta = metaByValue(record.exceStatus, getTransferExceStatusMetaMap(t), record.exceStatusText)
  const statusMeta = metaByValue(
    record.statusBool ?? record.status,
    getTransferStatusMetaMap(t),
    t('common.status.unknown')
  )
  const exceStatusMeta = metaByValue(
    record.exceStatus,
    getTransferExceStatusMetaMap(t),
    record.exceStatusText
  )
  const sourceMeta = metaByValue(record.source, getTransferSourceMetaMap(t), record.sourceText)
  return {
    ...record,
@@ -237,7 +335,8 @@
    typeLabel: normalizeText(record['type$'] || record.type) || '--',
    sourceText: sourceMeta.text,
    sourceTagType: sourceMeta.type,
    exceStatusText: normalizeText(record['exceStatus$'] || record.exceStatusText) || exceStatusMeta.text,
    exceStatusText:
      normalizeText(record['exceStatus$'] || record.exceStatusText) || exceStatusMeta.text,
    exceStatusTagType: exceStatusMeta.type,
    orgWareName: resolveWarehouseText(record, 'org') || '--',
    tarWareName: resolveWarehouseText(record, 'tar') || '--',
@@ -247,9 +346,11 @@
    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) || '--'
  }
}
@@ -259,8 +360,16 @@
}
export function normalizeTransferOrderRow(record = {}, t = $t) {
  const statusMeta = metaByValue(record.statusBool ?? record.status, getTransferStatusMetaMap(t), t('common.status.unknown'))
  const exceStatusMeta = metaByValue(record.exceStatus, getTransferExceStatusMetaMap(t), record.exceStatusText)
  const statusMeta = metaByValue(
    record.statusBool ?? record.status,
    getTransferStatusMetaMap(t),
    t('common.status.unknown')
  )
  const exceStatusMeta = metaByValue(
    record.exceStatus,
    getTransferExceStatusMetaMap(t),
    record.exceStatusText
  )
  return {
    ...record,
    id: record.id ?? null,
@@ -268,7 +377,8 @@
    poCode: normalizeText(record.poCode) || '--',
    typeLabel: normalizeText(record['type$'] || record.type) || '--',
    wkTypeLabel: normalizeText(record['wkType$'] || record.wkType) || '--',
    exceStatusText: normalizeText(record['exceStatus$'] || record.exceStatusText) || exceStatusMeta.text,
    exceStatusText:
      normalizeText(record['exceStatus$'] || record.exceStatusText) || exceStatusMeta.text,
    exceStatusTagType: exceStatusMeta.type,
    statusText: statusMeta.text,
    statusType: statusMeta.type,
@@ -277,12 +387,153 @@
    workQty: record.workQty ?? '--',
    qty: record.qty ?? '--',
    stationId: normalizeText(record.stationId) || '--',
    businessTimeText: normalizeText(record['businessTime$'] || record.businessTimeText || record.businessTime) || '--',
    businessTimeText:
      normalizeText(record['businessTime$'] || record.businessTimeText || record.businessTime) ||
      '--',
    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 buildTransferEditableItem(record = {}, fieldDefinitions = []) {
  return {
    __rowKey: createTransferItemRowKey(record),
    id: normalizeNumber(record.id, void 0),
    transferId: normalizeNumber(record.transferId, void 0),
    transferCode: normalizeText(record.transferCode || ''),
    matnrId: normalizeNumber(record.matnrId, void 0),
    maktx: normalizeText(record.maktx || ''),
    matnrCode: normalizeText(record.matnrCode || ''),
    anfme: normalizeNumber(record.anfme, 0) ?? 0,
    splrCode: normalizeText(record.splrCode || ''),
    splrName: normalizeText(record.splrName || ''),
    batch: normalizeText(record.batch || ''),
    unit: normalizeText(record.unit || record.stockUnit || ''),
    stockUnit: normalizeText(record.stockUnit || record.unit || ''),
    spec: normalizeText(record.spec || ''),
    model: normalizeText(record.model || ''),
    fieldsIndex: normalizeText(record.fieldsIndex || ''),
    platItemId: normalizeText(record.platItemId || ''),
    platOrderCode: normalizeText(record.platOrderCode || ''),
    platWorkCode: normalizeText(record.platWorkCode || ''),
    projectCode: normalizeText(record.projectCode || ''),
    status: normalizeNumber(record.status, 1) ?? 1,
    memo: normalizeText(record.memo || ''),
    ...resolveDynamicFieldValues(record, fieldDefinitions)
  }
}
export function createTransferEditableItemFromMaterial(record = {}, fieldDefinitions = []) {
  return {
    __rowKey: createTransferItemRowKey({
      ...record,
      id: void 0,
      matnrId: record.matnrId ?? record.id
    }),
    id: void 0,
    transferId: void 0,
    transferCode: '',
    matnrId: normalizeNumber(record.matnrId ?? record.id, void 0),
    maktx: normalizeText(record.maktx || ''),
    matnrCode: normalizeText(record.matnrCode || ''),
    anfme: normalizeNumber(record.anfme, 0) ?? 0,
    splrCode: '',
    splrName: '',
    batch: normalizeText(record.batch || ''),
    unit: normalizeText(record.unit || record.stockUnit || ''),
    stockUnit: normalizeText(record.stockUnit || record.unit || ''),
    spec: normalizeText(record.spec || ''),
    model: normalizeText(record.model || ''),
    fieldsIndex: normalizeText(record.fieldsIndex || ''),
    platItemId: '',
    platOrderCode: '',
    platWorkCode: '',
    projectCode: '',
    status: 1,
    memo: '',
    ...resolveDynamicFieldValues(record, fieldDefinitions)
  }
}
export function buildTransferItemsSavePayload(
  formData = {},
  areaOptions = [],
  fieldDefinitions = []
) {
  const transfer = buildTransferSavePayload(formData.transfer || formData, areaOptions)
  const rows = Array.isArray(formData.items) ? formData.items : []
  return {
    transfer,
    items: rows.map((item) => {
      const payload = {
        ...(item.id !== undefined && item.id !== null && item.id !== ''
          ? { id: normalizeNumber(item.id, void 0) }
          : {}),
        ...(item.transferId !== undefined && item.transferId !== null && item.transferId !== ''
          ? { transferId: normalizeNumber(item.transferId, void 0) }
          : {}),
        ...(normalizeText(item.transferCode)
          ? { transferCode: normalizeText(item.transferCode) }
          : {}),
        ...(item.matnrId !== undefined && item.matnrId !== null && item.matnrId !== ''
          ? { matnrId: normalizeNumber(item.matnrId, void 0) }
          : {}),
        ...(normalizeText(item.maktx) ? { maktx: normalizeText(item.maktx) } : {}),
        ...(normalizeText(item.matnrCode) ? { matnrCode: normalizeText(item.matnrCode) } : {}),
        ...(item.anfme !== undefined && item.anfme !== null && item.anfme !== ''
          ? { anfme: normalizeNumber(item.anfme, 0) }
          : {}),
        ...(normalizeText(item.splrCode) ? { splrCode: normalizeText(item.splrCode) } : {}),
        ...(normalizeText(item.splrName) ? { splrName: normalizeText(item.splrName) } : {}),
        ...(normalizeText(item.batch) ? { batch: normalizeText(item.batch) } : {}),
        ...(normalizeText(item.unit || item.stockUnit)
          ? {
              unit: normalizeText(item.unit || item.stockUnit),
              stockUnit: normalizeText(item.stockUnit || item.unit)
            }
          : {}),
        ...(normalizeText(item.spec) ? { spec: normalizeText(item.spec) } : {}),
        ...(normalizeText(item.model) ? { model: normalizeText(item.model) } : {}),
        ...(normalizeText(item.fieldsIndex)
          ? { fieldsIndex: normalizeText(item.fieldsIndex) }
          : {}),
        ...(normalizeText(item.platItemId) ? { platItemId: normalizeText(item.platItemId) } : {}),
        ...(normalizeText(item.platOrderCode)
          ? { platOrderCode: normalizeText(item.platOrderCode) }
          : {}),
        ...(normalizeText(item.platWorkCode)
          ? { platWorkCode: normalizeText(item.platWorkCode) }
          : {}),
        ...(normalizeText(item.projectCode)
          ? { projectCode: normalizeText(item.projectCode) }
          : {}),
        ...(item.status !== undefined && item.status !== null && item.status !== ''
          ? { status: normalizeNumber(item.status, 1) }
          : { status: 1 }),
        memo: normalizeText(item.memo || '')
      }
      fieldDefinitions.forEach((field) => {
        const value = item[field.fields]
        if (value !== undefined && value !== null && String(value).trim() !== '') {
          payload[field.fields] = value
        }
      })
      return payload
    })
  }
}
export function createTransferMaterialSearchState() {
  return {
    maktx: '',
    matnrCode: ''
  }
}
@@ -321,8 +572,17 @@
    { key: 'edit', label: $t('pages.orders.transfer.actions.edit'), icon: 'ri:pencil-line' }
  ]
  if (Number(normalizedRow.exceStatus) === 0) {
    actions.push({ key: 'publish', label: $t('pages.orders.transfer.actions.publish'), icon: 'ri:send-plane-line' })
    actions.push({ key: 'delete', label: $t('pages.orders.transfer.actions.delete'), icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
    actions.push({
      key: 'publish',
      label: $t('pages.orders.transfer.actions.publish'),
      icon: 'ri:send-plane-line'
    })
    actions.push({
      key: 'delete',
      label: $t('pages.orders.transfer.actions.delete'),
      icon: 'ri:delete-bin-5-line',
      color: 'var(--art-error)'
    })
  }
  return actions
}
@@ -336,7 +596,14 @@
      if (value === void 0) return null
      return {
        value,
        label: normalizeText(item.name || item.areaName || item.code || item.warehouseId$ || item.warehouseName || `${$t('menu.warehouseAreas')} ${value}`),
        label: normalizeText(
          item.name ||
            item.areaName ||
            item.code ||
            item.warehouseId$ ||
            item.warehouseName ||
            `${$t('menu.warehouseAreas')} ${value}`
        ),
        raw: item
      }
    })
@@ -352,7 +619,12 @@
      if (value === void 0) return null
      return {
        value,
        label: normalizeText(item.label || item.name || item.dictLabel || `${$t('pages.orders.transfer.dialog.type')} ${value}`)
        label: normalizeText(
          item.label ||
            item.name ||
            item.dictLabel ||
            `${$t('pages.orders.transfer.dialog.type')} ${value}`
        )
      }
    })
    .filter(Boolean)
rsf-design/src/views/orders/transfer/transferTable.columns.js
@@ -1,13 +1,34 @@
import { h } from 'vue'
import { ElTag } from 'element-plus'
import { ElLink, ElTag } from 'element-plus'
import { $t } from '@/locales'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
import { getTransferActionList } from './transferPage.helpers.js'
import TransferOrdersPanel from './modules/transfer-orders-panel.vue'
export function createTransferTableColumns({ handleActionClick } = {}) {
export function createTransferTableColumns({ handleActionClick, handleViewOrder } = {}) {
  return [
    {
      type: 'expand',
      width: 56,
      formatter: (row) => ({
        render() {
          return h(TransferOrdersPanel, {
            transferId: row.id,
            transferCode: row.code,
            onOrderView: handleViewOrder
          })
        }
      })
    },
    { 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: 'center',
      formatter: (row) => row.id ?? '--'
    },
    {
      prop: 'code',
      label: $t('pages.orders.transfer.search.code'),
@@ -28,7 +49,11 @@
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) =>
        h(ElTag, { type: row.sourceTagType || 'info', effect: 'light' }, () => row.sourceText || '--')
        h(
          ElTag,
          { type: row.sourceTagType || 'info', effect: 'light' },
          () => row.sourceText || '--'
        )
    },
    {
      prop: 'orgWareName',
@@ -63,7 +88,11 @@
      label: $t('pages.orders.transfer.search.exceStatus'),
      minWidth: 120,
      formatter: (row) =>
        h(ElTag, { type: row.exceStatusTagType || 'info', effect: 'light' }, () => row.exceStatusText || '--')
        h(
          ElTag,
          { type: row.exceStatusTagType || 'info', effect: 'light' },
          () => row.exceStatusText || '--'
        )
    },
    {
      prop: 'statusText',
@@ -72,6 +101,20 @@
      align: 'center',
      formatter: (row) =>
        h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '--')
    },
    {
      prop: 'updateByText',
      label: $t('table.updateBy'),
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) => row.updateByText || '--'
    },
    {
      prop: 'createByText',
      label: $t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) => row.createByText || '--'
    },
    {
      prop: 'updateTimeText',
@@ -109,7 +152,7 @@
  ]
}
export function createTransferOrderTableColumns() {
export function createTransferOrderTableColumns({ handleViewOrder } = {}) {
  return [
    { type: 'globalIndex', label: $t('table.index'), width: 72, align: 'center' },
    {
@@ -117,7 +160,18 @@
      label: $t('pages.orders.transfer.detail.relatedCode'),
      minWidth: 170,
      showOverflowTooltip: true,
      formatter: (row) => row.code || '--'
      formatter: (row) =>
        row.code && typeof handleViewOrder === 'function'
          ? h(
              ElLink,
              {
                type: 'primary',
                underline: 'hover',
                onClick: () => handleViewOrder(row)
              },
              () => row.code
            )
          : row.code || '--'
    },
    {
      prop: 'poCode',
@@ -145,7 +199,11 @@
      label: $t('pages.orders.transfer.detail.exceStatus'),
      minWidth: 120,
      formatter: (row) =>
        h(ElTag, { type: row.exceStatusTagType || 'info', effect: 'light' }, () => row.exceStatusText || '--')
        h(
          ElTag,
          { type: row.exceStatusTagType || 'info', effect: 'light' },
          () => row.exceStatusText || '--'
        )
    },
    {
      prop: 'statusText',