zhou zhou
19 小时以前 1d95b134d85c3c60cf0e72739888c9741a0bb1ee
#页面优化
4个文件已添加
10个文件已修改
3431 ■■■■ 已修改文件
rsf-design/src/api/wh-mat.js 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useAuth.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/en.json 234 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/zh.json 234 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js 89 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-station-area/basStationAreaTable.columns.js 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-dialog.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/index.vue 996 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-batch-dialog.vue 232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-batch-group-dialog.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-bind-loc-dialog.vue 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-dialog.vue 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/whMatPage.helpers.js 446 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/whMatTable.columns.js 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/wh-mat.js
@@ -4,13 +4,53 @@
  return String(value ?? '').trim()
}
function normalizeIds(ids) {
  if (Array.isArray(ids)) {
    return ids
      .map((item) => Number(item))
      .filter((item) => Number.isFinite(item))
      .join(',')
  }
  return String(ids ?? '')
    .split(',')
    .map((item) => Number(item))
    .filter((item) => Number.isFinite(item))
    .join(',')
}
function normalizeIdList(ids) {
  if (Array.isArray(ids)) {
    return ids.map((item) => Number(item)).filter((item) => Number.isFinite(item))
  }
  return String(ids ?? '')
    .split(',')
    .map((item) => Number(item))
    .filter((item) => Number.isFinite(item))
}
function normalizeQueryParams(params = {}) {
  const allowKeys = new Set([
    'condition',
    'code',
    'name',
    'spec',
    'model',
    'color',
    'size',
    'barcode',
    'groupId',
    'unit',
    'status',
    'flagCheck',
    'validWarn'
  ])
  const result = {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc'
  }
  ;['condition', 'code', 'name', 'spec', 'model', 'color', 'size', 'barcode', 'groupId'].forEach((key) => {
  Array.from(allowKeys).forEach((key) => {
    const value = params[key]
    if (value === undefined || value === null || value === '') {
      return
@@ -19,6 +59,29 @@
      const trimmed = normalizeText(value)
      if (trimmed) {
        result[key] = trimmed
      }
      return
    }
    result[key] = value
  })
  Object.entries(params).forEach(([key, value]) => {
    if (['current', 'pageSize', 'size', 'orderBy'].includes(key) || allowKeys.has(key)) {
      return
    }
    if (value === undefined || value === null || value === '') {
      return
    }
    if (typeof value === 'string') {
      const trimmed = normalizeText(value)
      if (trimmed) {
        result[key] = trimmed
      }
      return
    }
    if (Array.isArray(value)) {
      if (value.length) {
        result[key] = value
      }
      return
    }
@@ -45,6 +108,53 @@
  return request.get({ url: `/matnr/${id}` })
}
export function fetchGetMatnrMany(ids) {
  return request.post({ url: `/matnr/many/${normalizeIds(ids)}` })
}
export function fetchEnabledFields() {
  return request.get({ url: '/fields/enable/list' })
}
export function fetchSaveMatnr(params = {}) {
  return request.post({ url: '/matnr/save', params })
}
export function fetchUpdateMatnr(params = {}) {
  return request.post({ url: '/matnr/update', params })
}
export function fetchDeleteMatnr(ids) {
  return request.post({ url: `/matnr/remove/${normalizeIds(ids)}` })
}
export function fetchBindMatnrGroup(payload = {}) {
  return request.post({
    url: '/matnr/group/bind',
    params: {
      ids: normalizeIdList(payload.ids),
      ...(payload.groupId !== undefined && payload.groupId !== null && payload.groupId !== ''
        ? { groupId: Number(payload.groupId) }
        : {})
    }
  })
}
export function fetchBatchUpdateMatnr(payload = {}) {
  const matnr = payload.matnr && typeof payload.matnr === 'object' ? payload.matnr : {}
  return request.post({
    url: '/matnr/batch/update',
    params: {
      ids: normalizeIdList(payload.ids),
      matnr: Object.fromEntries(
        Object.entries(matnr).filter(
          ([, value]) => value !== undefined && value !== null && value !== ''
        )
      )
    }
  })
}
export function fetchMatnrGroupTree(params = {}) {
  return request.post({
    url: '/matnrGroup/tree',
@@ -52,3 +162,36 @@
  })
}
export async function fetchExportMatnrReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/matnr/export`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})
    },
    body: JSON.stringify(payload)
  })
}
export async function fetchDownloadMatnrTemplate(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/matnr/template/download`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})
    },
    body: JSON.stringify(payload)
  })
}
export function fetchImportMatnr(file) {
  const formData = new FormData()
  formData.append('file', file)
  return request.post({
    url: '/matnr/import',
    data: formData,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  })
}
rsf-design/src/hooks/core/useAuth.js
@@ -28,6 +28,7 @@
const BUTTON_ACTION_MAP = {
  query: 'list',
  add: 'save',
  update: 'update',
  edit: 'update',
  delete: 'remove'
}
rsf-design/src/locales/langs/en.json
@@ -182,20 +182,11 @@
  "setting": {
    "menuType": {
      "title": "Menu Layout",
      "list": [
        "Vertical",
        "Horizontal",
        "Mixed",
        "Dual"
      ]
      "list": ["Vertical", "Horizontal", "Mixed", "Dual"]
    },
    "theme": {
      "title": "Theme Style",
      "list": [
        "Light",
        "Dark",
        "System"
      ]
      "list": ["Light", "Dark", "System"]
    },
    "menu": {
      "title": "Menu Style"
@@ -205,17 +196,11 @@
    },
    "box": {
      "title": "Box Style",
      "list": [
        "Border",
        "Shadow"
      ]
      "list": ["Border", "Shadow"]
    },
    "container": {
      "title": "Container Width",
      "list": [
        "Full",
        "Boxed"
      ]
      "list": ["Full", "Boxed"]
    },
    "basics": {
      "title": "Basic Config",
@@ -659,21 +644,21 @@
      "send": "Send",
      "renameDialogTitle": "Rename Session",
      "sessionTitleField": "Session Title",
        "requestMetric": "Req: {value}",
        "sessionMetric": "Session: {id}",
        "promptMetric": "Prompt: {value}",
        "modelMetric": "Model: {value}",
        "promptLabel": "Prompt",
        "modelLabel": "Model",
        "modelSelectorLabel": "Chat Model",
        "modelSelectorHint": "Switching only affects subsequent replies in this session and does not change the global default model.",
        "modelSwitchFailed": "Failed to switch the chat model",
        "defaultModelSuffix": "(Default)",
        "mcpMetric": "MCP: {value}",
        "historyMetric": "History: {value}",
        "mcpLabel": "MCP",
        "historyLabel": "History",
        "recentMetric": "Recent: {value}",
      "requestMetric": "Req: {value}",
      "sessionMetric": "Session: {id}",
      "promptMetric": "Prompt: {value}",
      "modelMetric": "Model: {value}",
      "promptLabel": "Prompt",
      "modelLabel": "Model",
      "modelSelectorLabel": "Chat Model",
      "modelSelectorHint": "Switching only affects subsequent replies in this session and does not change the global default model.",
      "modelSwitchFailed": "Failed to switch the chat model",
      "defaultModelSuffix": "(Default)",
      "mcpMetric": "MCP: {value}",
      "historyMetric": "History: {value}",
      "mcpLabel": "MCP",
      "historyLabel": "History",
      "recentMetric": "Recent: {value}",
      "elapsedMetric": "Elapsed: {value} ms",
      "firstTokenMetric": "First token: {value} ms",
      "tokenMetric": "Tokens: prompt {prompt} / completion {completion} / total {total}",
@@ -848,12 +833,12 @@
          "systemPrompt": "System Prompt",
          "userPromptTemplate": "User Prompt Template"
        },
          "dialog": {
            "titleCreate": "Create Prompt",
            "titleEdit": "Edit Prompt",
            "titleDetail": "Prompt Detail",
            "defaultPreviewInput": "Please summarize the current input",
            "previewTitle": "Render Preview",
        "dialog": {
          "titleCreate": "Create Prompt",
          "titleEdit": "Edit Prompt",
          "titleDetail": "Prompt Detail",
          "defaultPreviewInput": "Please summarize the current input",
          "previewTitle": "Render Preview",
          "previewDescription": "Input sample content and metadata to preview the final rendering.",
          "previewAction": "Render Preview",
          "previewResolvedVariables": "Resolved variables: {value}",
@@ -2465,8 +2450,20 @@
      },
      "whMat": {
        "title": "Materials",
        "entity": "Material",
        "labels": {
          "allMaterials": "All Materials"
        },
        "actions": {
          "add": "Add Material",
          "batchGroup": "Batch Group",
          "batchWarn": "Batch Warning",
          "batchFlagCheck": "Batch QC",
          "batchStatus": "Batch Status",
          "batchStockLevel": "Batch Stock Level",
          "bindLoc": "Bind Location",
          "import": "Import",
          "downloadTemplate": "Download Template"
        },
        "search": {
          "groupKeywordPlaceholder": "Search material groups",
@@ -2478,19 +2475,76 @@
          "codePlaceholder": "Enter material code",
          "name": "Material Name",
          "namePlaceholder": "Enter material name",
          "groupId": "Material Group",
          "groupIdPlaceholder": "Select material group",
          "platCode": "Platform Code",
          "platCodePlaceholder": "Enter platform code",
          "spec": "Specification",
          "specPlaceholder": "Enter specification",
          "model": "Model",
          "modelPlaceholder": "Enter model",
          "color": "Color",
          "colorPlaceholder": "Enter color",
          "size": "Size",
          "sizePlaceholder": "Enter size",
          "unit": "Unit",
          "unitPlaceholder": "Enter unit",
          "purUnit": "Purchase Unit",
          "purUnitPlaceholder": "Enter purchase unit",
          "stockUnit": "Stock Unit",
          "stockUnitPlaceholder": "Enter stock unit",
          "barcode": "Barcode",
          "barcodePlaceholder": "Enter barcode"
          "barcodePlaceholder": "Enter barcode",
          "describle": "Description",
          "describlePlaceholder": "Enter description",
          "rglarId": "Batch Rule",
          "rglarIdPlaceholder": "Select batch rule",
          "weight": "Weight",
          "weightPlaceholder": "Enter weight",
          "nromNum": "Standard Pack Qty",
          "nromNumPlaceholder": "Enter standard pack qty",
          "stockLevel": "Stock Level",
          "stockLevelPlaceholder": "Select stock level",
          "flagLabelMange": "Label Management",
          "flagLabelMangePlaceholder": "Select label management",
          "safeQty": "Safe Qty",
          "safeQtyPlaceholder": "Enter safe qty",
          "minQty": "Min Qty",
          "minQtyPlaceholder": "Enter min qty",
          "maxQty": "Max Qty",
          "maxQtyPlaceholder": "Enter max qty",
          "stagn": "Stagnant Days",
          "stagnPlaceholder": "Enter stagnant days",
          "valid": "Shelf Life Days",
          "validPlaceholder": "Enter shelf life days",
          "validWarn": "Expiry Warning",
          "validWarnPlaceholder": "Enter expiry warning",
          "flagCheck": "Exempt Inspection",
          "flagCheckPlaceholder": "Select exempt inspection",
          "status": "Status",
          "statusPlaceholder": "Select status",
          "memo": "Memo",
          "memoPlaceholder": "Enter memo",
          "dynamicPlaceholder": "Enter {field}"
        },
        "messages": {
          "emptyGroups": "No material groups",
          "groupTimeout": "Material groups loading timed out and waiting has stopped",
          "groupLoadFailed": "Failed to load material groups",
          "serialRuleTimeout": "Batch rules loading timed out and waiting has stopped",
          "serialRuleLoadFailed": "Failed to load batch rules",
          "listTimeout": "Material list loading timed out and waiting has stopped",
          "listLoadFailed": "Failed to load material list",
          "detailTimeout": "Material detail timed out and waiting has stopped",
          "detailLoadFailed": "Failed to load material detail"
          "detailLoadFailed": "Failed to load material detail",
          "importSuccess": "Material import succeeded",
          "importFailed": "Material import failed",
          "templateDownloadSuccess": "Template downloaded successfully",
          "templateDownloadFailed": "Template download failed",
          "enabledFieldsTimeout": "Dynamic fields loading timed out",
          "bindLocTimeout": "Bind-location options loading timed out",
          "bindLocLoadFailed": "Failed to load bind-location options",
          "selectAtLeastOne": "Please select at least one material"
        },
        "table": {
          "code": "Material Code",
@@ -2501,6 +2555,102 @@
          "spec": "Specification",
          "model": "Model"
        },
        "dialog": {
          "titleCreate": "Add Material",
          "titleEdit": "Edit Material",
          "tabs": {
            "basic": "Basic Information",
            "control": "Control Information",
            "batchRule": "Batch Rule"
          },
          "fields": {
            "code": "Material Code",
            "name": "Material Name",
            "groupId": "Material Group",
            "useOrgName": "Using Organization",
            "spec": "Specification",
            "model": "Model",
            "color": "Color",
            "size": "Size",
            "weight": "Weight",
            "unit": "Unit",
            "purUnit": "Purchase Unit",
            "describle": "Description",
            "safeQty": "Safety Stock",
            "minQty": "Minimum Stock",
            "maxQty": "Maximum Stock",
            "stagn": "Stagnation Days",
            "valid": "Shelf Life Days",
            "validWarn": "Validity Warning Threshold",
            "flagCheck": "Exempt Inspection",
            "rglarId": "Batch Rule"
          },
          "placeholders": {
            "code": "Enter material code",
            "name": "Enter material name",
            "groupId": "Select material group",
            "useOrgName": "Enter using organization",
            "spec": "Enter specification",
            "model": "Enter model",
            "color": "Enter color",
            "size": "Enter size",
            "unit": "Enter unit",
            "purUnit": "Enter purchase unit",
            "describle": "Enter description",
            "flagCheck": "Select exempt inspection",
            "rglarId": "Select batch rule"
          },
          "validation": {
            "code": "Please enter material code",
            "name": "Please enter material name",
            "groupId": "Please select material group"
          }
        },
        "batchDialog": {
          "titles": {
            "status": "Batch Update Status",
            "stockLevel": "Batch Update Stock Level",
            "validWarn": "Batch Update Expiry Warning",
            "flagCheck": "Batch Update QC Status"
          },
          "fields": {
            "stockLevel": "Stock Level"
          },
          "placeholders": {
            "stockLevel": "Select stock level",
            "validWarn": "Enter expiry warning",
            "valid": "Enter shelf life days",
            "flagCheck": "Select exempt inspection"
          },
          "validation": {
            "status": "Please select status",
            "stockLevel": "Please select stock level",
            "validWarn": "Please enter expiry warning",
            "valid": "Please enter shelf life days",
            "flagCheck": "Please select exempt inspection"
          }
        },
        "batchGroupDialog": {
          "title": "Batch Update Material Group"
        },
        "bindLocDialog": {
          "title": "Bind Location",
          "fields": {
            "areaMatId": "Area Material",
            "areaId": "Area",
            "locId": "Location"
          },
          "placeholders": {
            "areaMatId": "Select area material",
            "areaId": "Select area",
            "locId": "Select locations"
          },
          "validation": {
            "areaMatId": "Please select area material",
            "areaId": "Please select area",
            "locId": "Please select locations"
          }
        },
        "detail": {
          "title": "Material Detail",
          "sections": {
rsf-design/src/locales/langs/zh.json
@@ -182,20 +182,11 @@
  "setting": {
    "menuType": {
      "title": "菜单布局",
      "list": [
        "垂直",
        "水平",
        "混合",
        "双列"
      ]
      "list": ["垂直", "水平", "混合", "双列"]
    },
    "theme": {
      "title": "主题风格",
      "list": [
        "浅色",
        "深色",
        "系统"
      ]
      "list": ["浅色", "深色", "系统"]
    },
    "menu": {
      "title": "菜单风格"
@@ -205,17 +196,11 @@
    },
    "box": {
      "title": "盒子样式",
      "list": [
        "边框",
        "阴影"
      ]
      "list": ["边框", "阴影"]
    },
    "container": {
      "title": "容器宽度",
      "list": [
        "铺满",
        "定宽"
      ]
      "list": ["铺满", "定宽"]
    },
    "basics": {
      "title": "基础配置",
@@ -661,21 +646,21 @@
      "send": "发送",
      "renameDialogTitle": "重命名会话",
      "sessionTitleField": "会话标题",
        "requestMetric": "Req: {value}",
        "sessionMetric": "Session: {id}",
        "promptMetric": "Prompt: {value}",
        "modelMetric": "Model: {value}",
        "promptLabel": "Prompt",
        "modelLabel": "Model",
        "modelSelectorLabel": "对话模型",
        "modelSelectorHint": "切换后仅影响当前会话后续回复,不会改动全局默认模型。",
        "modelSwitchFailed": "切换对话模型失败",
        "defaultModelSuffix": "(默认)",
        "mcpMetric": "MCP: {value}",
        "historyMetric": "History: {value}",
        "mcpLabel": "MCP",
        "historyLabel": "History",
        "recentMetric": "Recent: {value}",
      "requestMetric": "Req: {value}",
      "sessionMetric": "Session: {id}",
      "promptMetric": "Prompt: {value}",
      "modelMetric": "Model: {value}",
      "promptLabel": "Prompt",
      "modelLabel": "Model",
      "modelSelectorLabel": "对话模型",
      "modelSelectorHint": "切换后仅影响当前会话后续回复,不会改动全局默认模型。",
      "modelSwitchFailed": "切换对话模型失败",
      "defaultModelSuffix": "(默认)",
      "mcpMetric": "MCP: {value}",
      "historyMetric": "History: {value}",
      "mcpLabel": "MCP",
      "historyLabel": "History",
      "recentMetric": "Recent: {value}",
      "elapsedMetric": "耗时: {value} ms",
      "firstTokenMetric": "首包: {value} ms",
      "tokenMetric": "Tokens: prompt {prompt} / completion {completion} / total {total}",
@@ -850,12 +835,12 @@
          "systemPrompt": "系统提示词",
          "userPromptTemplate": "用户提示词模板"
        },
          "dialog": {
            "titleCreate": "新建 Prompt",
            "titleEdit": "编辑 Prompt",
            "titleDetail": "Prompt 详情",
            "defaultPreviewInput": "请根据当前输入给出摘要",
            "previewTitle": "渲染预览",
        "dialog": {
          "titleCreate": "新建 Prompt",
          "titleEdit": "编辑 Prompt",
          "titleDetail": "Prompt 详情",
          "defaultPreviewInput": "请根据当前输入给出摘要",
          "previewTitle": "渲染预览",
          "previewDescription": "输入示例内容和 metadata,直接预览最终渲染结果。",
          "previewAction": "渲染预览",
          "previewResolvedVariables": "已解析变量:{value}",
@@ -2473,8 +2458,20 @@
      },
      "whMat": {
        "title": "物料",
        "entity": "物料",
        "labels": {
          "allMaterials": "全部物料"
        },
        "actions": {
          "add": "新增物料",
          "batchGroup": "批量改分组",
          "batchWarn": "批量改预警",
          "batchFlagCheck": "批量质检",
          "batchStatus": "批量状态",
          "batchStockLevel": "批量库存等级",
          "bindLoc": "绑定库位",
          "import": "导入",
          "downloadTemplate": "下载模板"
        },
        "search": {
          "groupKeywordPlaceholder": "搜索物料分组",
@@ -2486,19 +2483,76 @@
          "codePlaceholder": "请输入物料编码",
          "name": "物料名称",
          "namePlaceholder": "请输入物料名称",
          "groupId": "物料分组",
          "groupIdPlaceholder": "请选择物料分组",
          "platCode": "平台编码",
          "platCodePlaceholder": "请输入平台编码",
          "spec": "规格",
          "specPlaceholder": "请输入规格",
          "model": "型号",
          "modelPlaceholder": "请输入型号",
          "color": "颜色",
          "colorPlaceholder": "请输入颜色",
          "size": "尺寸",
          "sizePlaceholder": "请输入尺寸",
          "unit": "单位",
          "unitPlaceholder": "请输入单位",
          "purUnit": "采购单位",
          "purUnitPlaceholder": "请输入采购单位",
          "stockUnit": "库位单位",
          "stockUnitPlaceholder": "请输入库位单位",
          "barcode": "条码",
          "barcodePlaceholder": "请输入条码"
          "barcodePlaceholder": "请输入条码",
          "describle": "描述",
          "describlePlaceholder": "请输入描述",
          "rglarId": "批次规则",
          "rglarIdPlaceholder": "请选择批次规则",
          "weight": "重量",
          "weightPlaceholder": "请输入重量",
          "nromNum": "标包数量",
          "nromNumPlaceholder": "请输入标包数量",
          "stockLevel": "库存等级",
          "stockLevelPlaceholder": "请选择库存等级",
          "flagLabelMange": "标签管理",
          "flagLabelMangePlaceholder": "请选择标签管理",
          "safeQty": "安全库存",
          "safeQtyPlaceholder": "请输入安全库存",
          "minQty": "最小库存",
          "minQtyPlaceholder": "请输入最小库存",
          "maxQty": "最大库存",
          "maxQtyPlaceholder": "请输入最大库存",
          "stagn": "停滞天数",
          "stagnPlaceholder": "请输入停滞天数",
          "valid": "保质期天数",
          "validPlaceholder": "请输入保质期天数",
          "validWarn": "效期预警阈值",
          "validWarnPlaceholder": "请输入效期预警阈值",
          "flagCheck": "是否免检",
          "flagCheckPlaceholder": "请选择是否免检",
          "status": "状态",
          "statusPlaceholder": "请选择状态",
          "memo": "备注",
          "memoPlaceholder": "请输入备注",
          "dynamicPlaceholder": "请输入{field}"
        },
        "messages": {
          "emptyGroups": "暂无物料分组",
          "groupTimeout": "物料分组加载超时,已停止等待",
          "groupLoadFailed": "获取物料分组失败",
          "serialRuleTimeout": "批次规则加载超时,已停止等待",
          "serialRuleLoadFailed": "获取批次规则失败",
          "listTimeout": "物料列表加载超时,已停止等待",
          "listLoadFailed": "获取物料列表失败",
          "detailTimeout": "物料详情加载超时,已停止等待",
          "detailLoadFailed": "获取物料详情失败"
          "detailLoadFailed": "获取物料详情失败",
          "importSuccess": "物料导入成功",
          "importFailed": "物料导入失败",
          "templateDownloadSuccess": "模板下载成功",
          "templateDownloadFailed": "模板下载失败",
          "enabledFieldsTimeout": "扩展字段加载超时,已停止等待",
          "bindLocTimeout": "绑定库位选项加载超时,已停止等待",
          "bindLocLoadFailed": "获取绑定库位选项失败",
          "selectAtLeastOne": "请至少选择一条物料记录"
        },
        "table": {
          "code": "物料编码",
@@ -2509,6 +2563,102 @@
          "spec": "规格",
          "model": "型号"
        },
        "dialog": {
          "titleCreate": "新增物料",
          "titleEdit": "编辑物料",
          "tabs": {
            "basic": "基础信息",
            "control": "控制信息",
            "batchRule": "批次规则"
          },
          "fields": {
            "code": "物料编码",
            "name": "物料名称",
            "groupId": "物料分组",
            "useOrgName": "使用组织",
            "spec": "规格",
            "model": "型号",
            "color": "颜色",
            "size": "尺寸",
            "weight": "重量",
            "unit": "单位",
            "purUnit": "采购单位",
            "describle": "描述",
            "safeQty": "安全库存",
            "minQty": "最小库存",
            "maxQty": "最大库存",
            "stagn": "停滞天数",
            "valid": "保质期天数",
            "validWarn": "效期预警阈值",
            "flagCheck": "是否免检",
            "rglarId": "批次规则"
          },
          "placeholders": {
            "code": "请输入物料编码",
            "name": "请输入物料名称",
            "groupId": "请选择物料分组",
            "useOrgName": "请输入使用组织",
            "spec": "请输入规格",
            "model": "请输入型号",
            "color": "请输入颜色",
            "size": "请输入尺寸",
            "unit": "请输入单位",
            "purUnit": "请输入采购单位",
            "describle": "请输入描述",
            "flagCheck": "请选择是否免检",
            "rglarId": "请选择批次规则"
          },
          "validation": {
            "code": "请输入物料编码",
            "name": "请输入物料名称",
            "groupId": "请选择物料分组"
          }
        },
        "batchDialog": {
          "titles": {
            "status": "批量修改状态",
            "stockLevel": "批量修改库存等级",
            "validWarn": "批量修改效期预警",
            "flagCheck": "批量修改质检状态"
          },
          "fields": {
            "stockLevel": "库存等级"
          },
          "placeholders": {
            "stockLevel": "请选择库存等级",
            "validWarn": "请输入效期预警阈值",
            "valid": "请输入保质期天数",
            "flagCheck": "请选择是否免检"
          },
          "validation": {
            "status": "请选择状态",
            "stockLevel": "请选择库存等级",
            "validWarn": "请输入效期预警阈值",
            "valid": "请输入保质期天数",
            "flagCheck": "请选择是否免检"
          }
        },
        "batchGroupDialog": {
          "title": "批量修改物料分组"
        },
        "bindLocDialog": {
          "title": "绑定库位",
          "fields": {
            "areaMatId": "库区物料",
            "areaId": "库区",
            "locId": "库位"
          },
          "placeholders": {
            "areaMatId": "请选择库区物料",
            "areaId": "请选择库区",
            "locId": "请选择库位"
          },
          "validation": {
            "areaMatId": "请选择库区物料",
            "areaId": "请选择库区",
            "locId": "请选择库位"
          }
        },
        "detail": {
          "title": "物料详情",
          "sections": {
rsf-design/src/views/basic-info/bas-station-area/basStationAreaPage.helpers.js
@@ -181,7 +181,13 @@
      }
      return {
        value: Number(value),
        label: normalizeText(item.name || item.areaName || item.code || item.areaCode || `${$t('menu.warehouseAreas')} ${value}`)
        label: normalizeText(
          item.name ||
            item.areaName ||
            item.code ||
            item.areaCode ||
            `${$t('menu.warehouseAreas')} ${value}`
        )
      }
    })
    .filter(Boolean)
@@ -203,7 +209,9 @@
      }
      return {
        value: Number(value),
        label: normalizeText(item.stationName || item.stationId || item.name || `${$t('menu.basStation')} ${value}`)
        label: normalizeText(
          item.stationName || item.stationId || item.name || `${$t('menu.basStation')} ${value}`
        )
      }
    })
    .filter(Boolean)
@@ -290,7 +298,9 @@
    containerType: normalizeText(params.containerType),
    barcode: normalizeText(params.barcode),
    autoTransfer:
      params.autoTransfer !== undefined && params.autoTransfer !== null && params.autoTransfer !== ''
      params.autoTransfer !== undefined &&
      params.autoTransfer !== null &&
      params.autoTransfer !== ''
        ? Number(params.autoTransfer)
        : void 0,
    stationAlias: normalizeText(params.stationAlias),
@@ -302,7 +312,9 @@
  }
  return Object.fromEntries(
    Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
    Object.entries(searchParams).filter(
      ([, value]) => value !== '' && value !== void 0 && value !== null
    )
  )
}
@@ -310,6 +322,7 @@
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc',
    ...buildBasStationAreaSearchParams(params)
  }
}
@@ -356,7 +369,9 @@
    ...(formData.area !== void 0 && formData.area !== null && formData.area !== ''
      ? { area: Number(formData.area) }
      : {}),
    ...(formData.isCrossZone !== void 0 && formData.isCrossZone !== null && formData.isCrossZone !== ''
    ...(formData.isCrossZone !== void 0 &&
    formData.isCrossZone !== null &&
    formData.isCrossZone !== ''
      ? { isCrossZone: Number(formData.isCrossZone) }
      : {}),
    ...(Array.isArray(formData.crossZoneArea) && formData.crossZoneArea.length
@@ -370,7 +385,9 @@
      ? { containerType: normalizeIdArray(formData.containerType) }
      : {}),
    barcode: normalizeText(formData.barcode) || '',
    ...(formData.autoTransfer !== void 0 && formData.autoTransfer !== null && formData.autoTransfer !== ''
    ...(formData.autoTransfer !== void 0 &&
    formData.autoTransfer !== null &&
    formData.autoTransfer !== ''
      ? { autoTransfer: Number(formData.autoTransfer) }
      : {}),
    ...(Array.isArray(formData.stationAlias) && formData.stationAlias.length
@@ -384,10 +401,12 @@
  }
}
export function buildBasStationAreaDialogModel(record = {}, resolvers = {}) {
export function buildBasStationAreaDialogModel(record = {}) {
  return {
    ...createBasStationAreaFormState(),
    ...(record.id !== void 0 && record.id !== null && record.id !== '' ? { id: Number(record.id) } : {}),
    ...(record.id !== void 0 && record.id !== null && record.id !== ''
      ? { id: Number(record.id) }
      : {}),
    stationAreaName: normalizeText(record.stationAreaName || ''),
    stationAreaId: normalizeText(record.stationAreaId || ''),
    type: normalizeIdValue(record.type),
@@ -395,13 +414,17 @@
    outAble: record.outAble !== void 0 && record.outAble !== null ? Number(record.outAble) : 0,
    useStatus: normalizeText(record.useStatus || ''),
    area: normalizeIdValue(record.area),
    isCrossZone: record.isCrossZone !== void 0 && record.isCrossZone !== null ? Number(record.isCrossZone) : 0,
    isCrossZone:
      record.isCrossZone !== void 0 && record.isCrossZone !== null ? Number(record.isCrossZone) : 0,
    crossZoneArea: normalizeIdArray(record.crossZoneArea),
    isWcs: record.isWcs !== void 0 && record.isWcs !== null ? Number(record.isWcs) : 0,
    wcsData: normalizeText(record.wcsData || ''),
    containerType: normalizeIdArray(record.containerType ?? record.containerTypes),
    barcode: normalizeText(record.barcode || ''),
    autoTransfer: record.autoTransfer !== void 0 && record.autoTransfer !== null ? Number(record.autoTransfer) : 0,
    autoTransfer:
      record.autoTransfer !== void 0 && record.autoTransfer !== null
        ? Number(record.autoTransfer)
        : 0,
    stationAlias: normalizeIdArray(record.stationAlias),
    status: record.status !== void 0 && record.status !== null ? Number(record.status) : 1,
    memo: normalizeText(record.memo || '')
@@ -424,32 +447,46 @@
    stationAreaName: normalizeText(record.stationAreaName) || t('common.placeholder.empty'),
    stationAreaId: normalizeText(record.stationAreaId) || t('common.placeholder.empty'),
    type: normalizeIdValue(record.type),
    typeText: normalizeText(
      record.type$ || record.typeText || resolvers.resolveTypeLabel?.(typeValue) || typeValue
    ) || t('common.placeholder.empty'),
    typeText:
      normalizeText(
        record.type$ || record.typeText || resolvers.resolveTypeLabel?.(typeValue) || typeValue
      ) || t('common.placeholder.empty'),
    inAble: normalizeIdValue(record.inAble),
    inAbleText: normalizeBooleanText(record.inAble, t),
    outAble: normalizeIdValue(record.outAble),
    outAbleText: normalizeBooleanText(record.outAble, t),
    useStatus: normalizeText(record.useStatus),
    useStatusText:
      normalizeText(record.useStatus$ || record.useStatusText || resolvers.resolveUseStatusLabel?.(record.useStatus) || record.useStatus) ||
      t('common.placeholder.empty'),
      normalizeText(
        record.useStatus$ ||
          record.useStatusText ||
          resolvers.resolveUseStatusLabel?.(record.useStatus) ||
          record.useStatus
      ) || t('common.placeholder.empty'),
    area: normalizeIdValue(areaId),
    areaText: normalizeText(record.area$ || record.areaText || resolvers.resolveAreaLabel?.(areaId) || '') || t('common.placeholder.empty'),
    areaText:
      normalizeText(
        record.area$ || record.areaText || resolvers.resolveAreaLabel?.(areaId) || ''
      ) || t('common.placeholder.empty'),
    isCrossZone: normalizeIdValue(record.isCrossZone),
    isCrossZoneText: normalizeBooleanText(record.isCrossZone, t),
    crossZoneArea: crossZoneAreaIds,
    crossZoneAreaText:
      resolveOptionText(crossZoneAreaIds, resolvers.resolveCrossZoneAreaLabel, record.crossZoneAreaText || []) ||
      t('common.placeholder.empty'),
      resolveOptionText(
        crossZoneAreaIds,
        resolvers.resolveCrossZoneAreaLabel,
        record.crossZoneAreaText || []
      ) || t('common.placeholder.empty'),
    isWcs: normalizeIdValue(record.isWcs),
    isWcsText: normalizeBooleanText(record.isWcs, t),
    wcsData: normalizeText(record.wcsData) || t('common.placeholder.empty'),
    containerType: containerTypeIds,
    containerTypeText:
      resolveOptionText(containerTypeIds, resolvers.resolveContainerTypeLabel, record.containerTypesText || []) ||
      t('common.placeholder.empty'),
      resolveOptionText(
        containerTypeIds,
        resolvers.resolveContainerTypeLabel,
        record.containerTypesText || []
      ) || t('common.placeholder.empty'),
    barcode: normalizeText(record.barcode) || t('common.placeholder.empty'),
    autoTransfer: normalizeIdValue(record.autoTransfer),
    autoTransferText: normalizeBooleanText(record.autoTransfer, t),
@@ -465,10 +502,14 @@
    statusType: statusMeta.type,
    statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
    memo: normalizeText(record.memo) || t('common.placeholder.empty'),
    createByText: normalizeText(record.createBy$ || record.createByText || '') || t('common.placeholder.empty'),
    createTimeText: normalizeText(record.createTime$ || record.createTime || '') || t('common.placeholder.empty'),
    updateByText: normalizeText(record.updateBy$ || record.updateByText || '') || t('common.placeholder.empty'),
    updateTimeText: normalizeText(record.updateTime$ || record.updateTime || '') || t('common.placeholder.empty')
    createByText:
      normalizeText(record.createBy$ || record.createByText || '') || t('common.placeholder.empty'),
    createTimeText:
      normalizeText(record.createTime$ || record.createTime || '') || t('common.placeholder.empty'),
    updateByText:
      normalizeText(record.updateBy$ || record.updateByText || '') || t('common.placeholder.empty'),
    updateTimeText:
      normalizeText(record.updateTime$ || record.updateTime || '') || t('common.placeholder.empty')
  }
}
rsf-design/src/views/basic-info/bas-station-area/basStationAreaTable.columns.js
@@ -19,7 +19,12 @@
  }
  if (canDelete && handleDelete) {
    operations.push({ key: 'delete', label: t('common.actions.delete'), icon: 'ri:delete-bin-5-line', color: 'var(--art-error)' })
    operations.push({
      key: 'delete',
      label: t('common.actions.delete'),
      icon: 'ri:delete-bin-5-line',
      color: 'var(--art-error)'
    })
  }
  return [
@@ -33,6 +38,13 @@
      label: t('table.index'),
      width: 72,
      align: 'center'
    },
    {
      prop: 'id',
      label: t('table.id'),
      width: 90,
      align: 'center',
      formatter: (row) => row.id ?? '--'
    },
    {
      prop: 'stationAreaId',
@@ -138,11 +150,21 @@
      width: 96,
      align: 'center',
      formatter: (row) => {
        const status = getBasStationAreaStatusOptions(t).find((item) => Number(item.value) === Number(row.status))
        const status = getBasStationAreaStatusOptions(t).find(
          (item) => Number(item.value) === Number(row.status)
        )
        const text = status?.label || row.statusText || '--'
        const type = Number(row.status) === 1 ? 'success' : Number(row.status) === 0 ? 'danger' : 'info'
        const type =
          Number(row.status) === 1 ? 'success' : Number(row.status) === 0 ? 'danger' : 'info'
        return h(ElTag, { type, effect: 'light' }, () => text)
      }
    },
    {
      prop: 'updateByText',
      label: t('table.updateBy'),
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) => row.updateByText || '--'
    },
    {
      prop: 'updateTimeText',
@@ -152,6 +174,20 @@
      formatter: (row) => row.updateTimeText || '--'
    },
    {
      prop: 'createByText',
      label: t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true,
      formatter: (row) => row.createByText || '--'
    },
    {
      prop: 'createTimeText',
      label: t('table.createTime'),
      minWidth: 170,
      showOverflowTooltip: true,
      formatter: (row) => row.createTimeText || '--'
    },
    {
      prop: 'memo',
      label: t('pages.basicInfo.basStationArea.search.memo'),
      minWidth: 180,
rsf-design/src/views/basic-info/bas-station-area/modules/bas-station-area-dialog.vue
@@ -36,9 +36,7 @@
  import {
    buildBasStationAreaDialogModel,
    createBasStationAreaFormState,
    getBasStationAreaBinaryOptions,
    getBasStationAreaStatusOptions,
    getBasStationAreaTypeOptions
    getBasStationAreaStatusOptions
  } from '../basStationAreaPage.helpers'
  const props = defineProps({
@@ -48,7 +46,8 @@
    areaOptions: { type: Array, default: () => [] },
    crossZoneAreaOptions: { type: Array, default: () => [] },
    containerTypeOptions: { type: Array, default: () => [] },
    stationOptions: { type: Array, default: () => [] }
    stationOptions: { type: Array, default: () => [] },
    useStatusOptions: { type: Array, default: () => [] }
  })
  const emit = defineEmits(['update:visible', 'submit'])
@@ -58,19 +57,47 @@
  const isEdit = computed(() => props.dialogType === 'edit')
  const dialogTitle = computed(() =>
    t(isEdit.value ? 'pages.basicInfo.basStationArea.dialog.titleEdit' : 'pages.basicInfo.basStationArea.dialog.titleAdd')
    t(
      isEdit.value
        ? 'pages.basicInfo.basStationArea.dialog.titleEdit'
        : 'pages.basicInfo.basStationArea.dialog.titleAdd'
    )
  )
  const rules = computed(() => ({
    stationAreaName: [{ required: true, message: t('pages.basicInfo.basStationArea.dialog.validation.stationAreaName'), trigger: 'blur' }],
    stationAreaId: [{ required: true, message: t('pages.basicInfo.basStationArea.dialog.validation.stationAreaId'), trigger: 'blur' }],
    type: [{ required: true, message: t('pages.basicInfo.basStationArea.dialog.validation.type'), trigger: 'change' }],
    area: [{ required: true, message: t('pages.basicInfo.basStationArea.dialog.validation.area'), trigger: 'change' }],
    containerType: [{ type: 'array', required: true, message: t('pages.basicInfo.basStationArea.dialog.validation.containerType'), trigger: 'change' }],
    stationAlias: [{ type: 'array', required: true, message: t('pages.basicInfo.basStationArea.dialog.validation.stationAlias'), trigger: 'change' }]
    stationAreaName: [
      {
        required: true,
        message: t('pages.basicInfo.basStationArea.dialog.validation.stationAreaName'),
        trigger: 'blur'
      }
    ],
    stationAreaId: [
      {
        required: true,
        message: t('pages.basicInfo.basStationArea.dialog.validation.stationAreaId'),
        trigger: 'blur'
      }
    ],
    containerType: [
      {
        type: 'array',
        required: true,
        message: t('pages.basicInfo.basStationArea.dialog.validation.containerType'),
        trigger: 'change'
      }
    ],
    stationAlias: [
      {
        type: 'array',
        required: true,
        message: t('pages.basicInfo.basStationArea.dialog.validation.stationAlias'),
        trigger: 'change'
      }
    ]
  }))
  const formItems = computed(() => [
  const baseLegacyItems = computed(() => [
    {
      label: t('pages.basicInfo.basStationArea.dialog.stationAreaName'),
      key: 'stationAreaName',
@@ -81,42 +108,12 @@
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.stationAreaId'),
      key: 'stationAreaId',
      type: 'input',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.placeholder.stationAreaId'),
        clearable: true
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.type'),
      key: 'type',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.type'),
        clearable: true,
        options: getBasStationAreaTypeOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.area'),
      key: 'area',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.area'),
        clearable: true,
        filterable: true,
        options: props.areaOptions || []
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.crossZoneArea'),
      label: t('pages.basicInfo.basStation.table.crossZoneArea'),
      key: 'crossZoneArea',
      type: 'select',
      span: 24,
      props: {
        placeholder: t('pages.basicInfo.basStationArea.placeholder.crossZoneArea'),
        placeholder: t('pages.basicInfo.basStation.table.crossZoneArea'),
        clearable: true,
        multiple: true,
        collapseTags: true,
@@ -125,17 +122,26 @@
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.containerType'),
      label: t('pages.basicInfo.basStation.table.containerTypes'),
      key: 'containerType',
      type: 'select',
      span: 24,
      props: {
        placeholder: t('pages.basicInfo.basStationArea.placeholder.containerType'),
        placeholder: t('pages.basicInfo.basStation.table.containerTypes'),
        clearable: true,
        multiple: true,
        collapseTags: true,
        filterable: true,
        options: props.containerTypeOptions || []
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.stationAreaId'),
      key: 'stationAreaId',
      type: 'input',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.placeholder.stationAreaId'),
        clearable: true
      }
    },
    {
@@ -151,101 +157,22 @@
        filterable: true,
        options: props.stationOptions || []
      }
    },
    }
  ])
  const createOnlyItems = computed(() => [
    {
      label: t('pages.basicInfo.basStationArea.dialog.inAble'),
      key: 'inAble',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.inAble'),
        clearable: true,
        options: getBasStationAreaBinaryOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.outAble'),
      key: 'outAble',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.outAble'),
        clearable: true,
        options: getBasStationAreaBinaryOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.isCrossZone'),
      key: 'isCrossZone',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.isCrossZone'),
        clearable: true,
        options: getBasStationAreaBinaryOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.isWcs'),
      key: 'isWcs',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.isWcs'),
        clearable: true,
        options: getBasStationAreaBinaryOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.autoTransfer'),
      key: 'autoTransfer',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.autoTransfer'),
        clearable: true,
        options: getBasStationAreaBinaryOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.useStatus'),
      key: 'useStatus',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.useStatus'),
        clearable: true,
        filterable: true,
        options: props.useStatusOptions || []
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.wcsData'),
      key: 'wcsData',
      type: 'input',
      span: 24,
      props: {
        type: 'textarea',
        rows: 3,
        placeholder: t('pages.basicInfo.basStationArea.placeholder.wcsData'),
        clearable: true
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.barcode'),
      key: 'barcode',
      type: 'input',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.placeholder.barcode'),
        clearable: true
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.status'),
      label: t('table.status'),
      key: 'status',
      type: 'select',
      props: {
        placeholder: t('pages.basicInfo.basStationArea.search.status'),
        placeholder: t('table.status'),
        clearable: true,
        options: getBasStationAreaStatusOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.basStationArea.dialog.memo'),
      label: t('table.memo'),
      key: 'memo',
      type: 'input',
      span: 24,
@@ -257,6 +184,10 @@
      }
    }
  ])
  const formItems = computed(() =>
    isEdit.value ? baseLegacyItems.value : [...baseLegacyItems.value, ...createOnlyItems.value]
  )
  const resetForm = () => {
    Object.assign(form, createBasStationAreaFormState())
@@ -271,7 +202,25 @@
    if (!formRef.value) return
    try {
      await formRef.value.validate()
      emit('submit', { ...form })
      const payload = isEdit.value
        ? {
            id: form.id,
            stationAreaName: form.stationAreaName,
            crossZoneArea: Array.isArray(form.crossZoneArea) ? [...form.crossZoneArea] : [],
            containerType: Array.isArray(form.containerType) ? [...form.containerType] : [],
            stationAreaId: form.stationAreaId,
            stationAlias: Array.isArray(form.stationAlias) ? [...form.stationAlias] : []
          }
        : {
            stationAreaName: form.stationAreaName,
            crossZoneArea: Array.isArray(form.crossZoneArea) ? [...form.crossZoneArea] : [],
            containerType: Array.isArray(form.containerType) ? [...form.containerType] : [],
            stationAreaId: form.stationAreaId,
            stationAlias: Array.isArray(form.stationAlias) ? [...form.stationAlias] : [],
            status: form.status,
            memo: form.memo
          }
      emit('submit', payload)
    } catch {
      return
    }
rsf-design/src/views/basic-info/wh-mat/index.vue
@@ -5,7 +5,9 @@
        <ElCard class="wh-mat-page__sidebar-card">
          <div class="mb-3 flex items-center justify-between gap-3">
            <div>
              <div class="text-base font-medium text-[var(--art-text-primary)]">{{ t('pages.basicInfo.whMat.title') }}</div>
              <div class="text-base font-medium text-[var(--art-text-primary)]">{{
                t('pages.basicInfo.whMat.title')
              }}</div>
              <div class="text-xs text-[var(--art-text-secondary)]">
                {{ selectedGroupLabel }}
              </div>
@@ -28,21 +30,26 @@
            <div v-if="groupTreeLoading" class="py-6">
              <ElSkeleton :rows="10" animated />
            </div>
            <ElEmpty v-else-if="!groupTreeData.length" :description="t('pages.basicInfo.whMat.messages.emptyGroups')" />
            <ElEmpty
              v-else-if="!groupTreeData.length"
              :description="t('pages.basicInfo.whMat.messages.emptyGroups')"
            />
            <ElTree
              v-else
              :data="groupTreeData"
              :props="treeProps"
              node-key="id"
              highlight-current
              default-expand-all
              :default-expanded-keys="defaultExpandedGroupKeys"
              :current-node-key="selectedGroupId"
              @node-click="handleGroupNodeClick"
            >
              <template #default="{ data }">
                <div class="flex items-center gap-2">
                  <span class="font-medium">{{ data.name || t('common.placeholder.empty') }}</span>
                  <span class="text-xs text-[var(--art-text-secondary)]">{{ data.code || t('common.placeholder.empty') }}</span>
                  <span class="text-xs text-[var(--art-text-secondary)]">{{
                    data.code || t('common.placeholder.empty')
                  }}</span>
                </div>
              </template>
            </ElTree>
@@ -60,23 +67,155 @@
        />
        <ElCard class="art-table-card">
          <ArtTableHeader :loading="loading" v-model:columns="columnChecks" @refresh="loadMatnrList" />
          <ArtTableHeader
            :loading="loading"
            v-model:columns="columnChecks"
            @refresh="handleRefresh"
          >
            <template #left>
              <ElSpace wrap>
                <ElButton v-auth="'add'" @click="handleShowDialog('add')" v-ripple>
                  {{ t('pages.basicInfo.whMat.actions.add') }}
                </ElButton>
                <ElButton
                  v-if="showBatchActionButtons"
                  v-auth="'update'"
                  :disabled="selectedRows.length === 0"
                  @click="openBatchGroupDialog"
                  v-ripple
                >
                  {{ t('pages.basicInfo.whMat.actions.batchGroup') }}
                </ElButton>
                <ElButton
                  v-if="showBatchActionButtons"
                  v-auth="'update'"
                  :disabled="selectedRows.length === 0"
                  @click="openBatchDialog('validWarn')"
                  v-ripple
                >
                  {{ t('pages.basicInfo.whMat.actions.batchWarn') }}
                </ElButton>
                <ElButton
                  v-if="showBatchActionButtons"
                  v-auth="'update'"
                  :disabled="selectedRows.length === 0"
                  @click="openBatchDialog('flagCheck')"
                  v-ripple
                >
                  {{ t('pages.basicInfo.whMat.actions.batchFlagCheck') }}
                </ElButton>
                <ElButton
                  v-if="showBatchActionButtons"
                  v-auth="'update'"
                  :disabled="selectedRows.length === 0"
                  @click="openBatchDialog('status')"
                  v-ripple
                >
                  {{ t('pages.basicInfo.whMat.actions.batchStatus') }}
                </ElButton>
                <ElButton
                  v-if="showBatchActionButtons"
                  v-auth="'update'"
                  :disabled="selectedRows.length === 0"
                  @click="openBatchDialog('stockLevel')"
                  v-ripple
                >
                  {{ t('pages.basicInfo.whMat.actions.batchStockLevel') }}
                </ElButton>
                <ElButton
                  v-if="showBatchActionButtons"
                  v-auth="'update'"
                  :disabled="selectedRows.length === 0"
                  @click="openBindLocDialog"
                  v-ripple
                >
                  {{ t('pages.basicInfo.whMat.actions.bindLoc') }}
                </ElButton>
                <ElButton
                  v-auth="'delete'"
                  type="danger"
                  :disabled="selectedRows.length === 0"
                  @click="handleBatchDelete"
                  v-ripple
                >
                  {{ t('common.actions.batchDelete') }}
                </ElButton>
                <div v-auth="'update'">
                  <ElUpload
                    :auto-upload="false"
                    :show-file-list="false"
                    accept=".xlsx,.xls"
                    @change="handleImportFileChange"
                  >
                    <ElButton :loading="importing" v-ripple>
                      {{ t('pages.basicInfo.whMat.actions.import') }}
                    </ElButton>
                  </ElUpload>
                </div>
                <ElButton :loading="templateDownloading" @click="handleDownloadTemplate" v-ripple>
                  {{ t('pages.basicInfo.whMat.actions.downloadTemplate') }}
                </ElButton>
                <ListExportPrint
                  class="inline-flex"
                  :preview-visible="previewVisible"
                  @update:previewVisible="handlePreviewVisibleChange"
                  :report-title="reportTitle"
                  :selected-rows="selectedRows"
                  :query-params="reportQueryParams"
                  :columns="columns"
                  :preview-rows="previewRows"
                  :preview-meta="resolvedPreviewMeta"
                  :total="pagination.total"
                  :disabled="loading"
                  @export="handleExport"
                  @print="handlePrint"
                />
              </ElSpace>
            </template>
          </ArtTableHeader>
          <ArtTable
            :loading="loading"
            :data="tableData"
            :columns="columns"
            :pagination="pagination"
            row-key="id"
            @selection-change="handleSelectionChange"
            @pagination:size-change="handleSizeChange"
            @pagination:current-change="handleCurrentChange"
          >
            <template #action="{ row }">
              <ArtButtonTable icon="ri:eye-line" @click="openDetailDrawer(row)" />
            </template>
          </ArtTable>
          />
        </ElCard>
      </div>
    </div>
    <WhMatDialog
      v-model:visible="dialogVisible"
      :dialog-type="dialogType"
      :material-data="currentMaterialData"
      :group-options="groupOptions"
      :serial-rule-options="serialRuleOptions"
      @submit="handleDialogSubmit"
    />
    <WhMatBatchDialog
      v-model:visible="batchDialogVisible"
      :action-type="batchDialogType"
      @submit="handleBatchDialogSubmit"
    />
    <WhMatBatchGroupDialog
      v-model:visible="batchGroupDialogVisible"
      :group-options="groupOptions"
      @submit="handleBatchGroupSubmit"
    />
    <WhMatBindLocDialog
      v-model:visible="bindLocDialogVisible"
      :area-mat-options="areaMatOptions"
      :area-options="areaOptions"
      :loc-options="locOptions"
      @submit="handleBindLocSubmit"
    />
    <WhMatDetailDrawer
      v-model:visible="detailDrawerVisible"
@@ -87,38 +226,97 @@
</template>
<script setup>
  import { ElMessage } from 'element-plus'
  import { computed, onMounted, reactive, ref } from 'vue'
  import { ElMessage } from 'element-plus'
  import { useI18n } from 'vue-i18n'
  import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
  import ListExportPrint from '@/components/biz/list-export-print/index.vue'
  import { useAuth } from '@/hooks/core/useAuth'
  import { useTableColumns } from '@/hooks/core/useTableColumns'
  import { useUserStore } from '@/store/modules/user'
  import { fetchSerialRulePage } from '@/api/system-manage'
  import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
  import { fetchLocPage } from '@/api/loc'
  import { fetchLocAreaMatList } from '@/api/loc-area-mat'
  import { fetchBindLocAreaMatRelaByMatnr } from '@/api/loc-area-mat-rela'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { fetchMatnrDetail, fetchMatnrGroupTree, fetchMatnrPage } from '@/api/wh-mat'
  import { useCrudPage } from '@/views/system/common/useCrudPage'
  import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
  import {
    fetchBatchUpdateMatnr,
    fetchBindMatnrGroup,
    fetchDeleteMatnr,
    fetchDownloadMatnrTemplate,
    fetchEnabledFields,
    fetchExportMatnrReport,
    fetchGetMatnrMany,
    fetchImportMatnr,
    fetchMatnrDetail,
    fetchMatnrGroupTree,
    fetchMatnrPage,
    fetchSaveMatnr,
    fetchUpdateMatnr
  } from '@/api/wh-mat'
  import WhMatBatchDialog from './modules/wh-mat-batch-dialog.vue'
  import WhMatBatchGroupDialog from './modules/wh-mat-batch-group-dialog.vue'
  import WhMatBindLocDialog from './modules/wh-mat-bind-loc-dialog.vue'
  import WhMatDialog from './modules/wh-mat-dialog.vue'
  import WhMatDetailDrawer from './modules/wh-mat-detail-drawer.vue'
  import { createWhMatTableColumns } from './whMatTable.columns'
  import {
    WH_MAT_REPORT_STYLE,
    WH_MAT_REPORT_TITLE,
    buildMatnrGroupTreeQueryParams,
    buildMatnrPageQueryParams,
    buildWhMatDialogModel,
    buildWhMatPrintRows,
    buildWhMatReportMeta,
    buildWhMatSavePayload,
    createWhMatSearchState,
    getWhMatDynamicFieldKey,
    getWhMatFlagLabelManageOptions,
    getWhMatFlagCheckOptions,
    getWhMatStockLevelOptions,
    getWhMatStatusOptions,
    getWhMatTreeNodeLabel,
    normalizeMatnrDetail,
    normalizeWhMatEnabledFields,
    normalizeMatnrGroupTreeRows,
    normalizeMatnrRow
    normalizeMatnrRow,
    resolveWhMatGroupOptions,
    resolveWhMatSerialRuleOptions
  } from './whMatPage.helpers'
  defineOptions({ name: 'WhMat' })
  const { t } = useI18n()
  const { t } = useI18n()
  const { hasAuth } = useAuth()
  const userStore = useUserStore()
  const showBatchActionButtons = false
  const loading = ref(false)
  const groupTreeLoading = ref(false)
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const batchDialogVisible = ref(false)
  const batchGroupDialogVisible = ref(false)
  const bindLocDialogVisible = ref(false)
  const bindLocOptionsLoading = ref(false)
  const importing = ref(false)
  const templateDownloading = ref(false)
  const tableData = ref([])
  const groupTreeData = ref([])
  const detailData = ref({})
  const enabledFields = ref([])
  const serialRuleOptions = ref([])
  const areaOptions = ref([])
  const areaMatOptions = ref([])
  const locOptions = ref([])
  const selectedGroupId = ref(null)
  const groupSearch = ref('')
  const batchDialogType = ref('status')
  const searchForm = ref(createWhMatSearchState())
  let handleDeleteAction = null
  const pagination = reactive({
    current: 1,
@@ -130,6 +328,18 @@
    label: 'name',
    children: 'children'
  }
  const reportTitle = WH_MAT_REPORT_TITLE
  const groupOptions = computed(() => resolveWhMatGroupOptions(groupTreeData.value))
  const defaultExpandedGroupKeys = computed(() => collectExpandedGroupKeys(groupTreeData.value))
  const reportQueryParams = computed(() =>
    buildMatnrPageQueryParams({
      ...searchForm.value,
      groupId: searchForm.value?.groupId || selectedGroupId.value,
      current: 1,
      pageSize: pagination.size
    })
  )
  const searchItems = computed(() => [
    {
@@ -160,12 +370,65 @@
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.groupId'),
      key: 'groupId',
      type: 'treeselect',
      props: {
        data: groupOptions.value,
        props: {
          label: 'displayLabel',
          value: 'value',
          children: 'children'
        },
        checkStrictly: true,
        defaultExpandAll: true,
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.groupIdPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.platCode'),
      key: 'platCode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.platCodePlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.spec'),
      key: 'spec',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.specPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.model'),
      key: 'model',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.modelPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.color'),
      key: 'color',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.colorPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.size'),
      key: 'size',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.sizePlaceholder')
      }
    },
    {
@@ -176,13 +439,214 @@
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.barcodePlaceholder')
      }
    }
    },
    {
      label: t('pages.basicInfo.whMat.search.unit'),
      key: 'unit',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.unitPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.purUnit'),
      key: 'purUnit',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.purUnitPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.stockUnit'),
      key: 'stockUnit',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.stockUnitPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.describle'),
      key: 'describle',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.describlePlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.rglarId'),
      key: 'rglarId',
      type: 'select',
      props: {
        clearable: true,
        filterable: true,
        placeholder: t('pages.basicInfo.whMat.search.rglarIdPlaceholder'),
        options: serialRuleOptions.value
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.weight'),
      key: 'weight',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        valueOnClear: null,
        placeholder: t('pages.basicInfo.whMat.search.weightPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.nromNum'),
      key: 'nromNum',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        valueOnClear: null,
        placeholder: t('pages.basicInfo.whMat.search.nromNumPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.stockLevel'),
      key: 'stockLevel',
      type: 'select',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.stockLevelPlaceholder'),
        options: getWhMatStockLevelOptions()
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.flagLabelMange'),
      key: 'flagLabelMange',
      type: 'select',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.flagLabelMangePlaceholder'),
        options: getWhMatFlagLabelManageOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.safeQty'),
      key: 'safeQty',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        valueOnClear: null,
        placeholder: t('pages.basicInfo.whMat.search.safeQtyPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.minQty'),
      key: 'minQty',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        valueOnClear: null,
        placeholder: t('pages.basicInfo.whMat.search.minQtyPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.maxQty'),
      key: 'maxQty',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        valueOnClear: null,
        placeholder: t('pages.basicInfo.whMat.search.maxQtyPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.stagn'),
      key: 'stagn',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        valueOnClear: null,
        placeholder: t('pages.basicInfo.whMat.search.stagnPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.valid'),
      key: 'valid',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        valueOnClear: null,
        placeholder: t('pages.basicInfo.whMat.search.validPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.validWarn'),
      key: 'validWarn',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        valueOnClear: null,
        placeholder: t('pages.basicInfo.whMat.search.validWarnPlaceholder')
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.flagCheck'),
      key: 'flagCheck',
      type: 'select',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.flagCheckPlaceholder'),
        options: getWhMatFlagCheckOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.statusPlaceholder'),
        options: getWhMatStatusOptions(t)
      }
    },
    {
      label: t('pages.basicInfo.whMat.search.memo'),
      key: 'memo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.memoPlaceholder')
      }
    },
    ...enabledFields.value.map((field) => ({
      label: field.fieldsAlise,
      key: getWhMatDynamicFieldKey(field.fields),
      type: 'input',
      props: {
        clearable: true,
        placeholder: t('pages.basicInfo.whMat.search.dynamicPlaceholder', {
          field: field.fieldsAlise
        })
      }
    }))
  ])
  const { columnChecks, columns } = useTableColumns(() =>
  const { columnChecks, columns, resetColumns } = useTableColumns(() =>
    createWhMatTableColumns({
      t,
      handleViewDetail: openDetailDrawer
      enabledFields: enabledFields.value,
      handleViewDetail: openDetailDrawer,
      handleEdit: hasAuth('update') ? openEditDialog : null,
      handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
      handlePrint: (row) => handlePrint({ ids: [row.id] }),
      canEdit: hasAuth('update'),
      canDelete: hasAuth('delete'),
      t
    })
  )
@@ -208,6 +672,46 @@
      }
    }
    return null
  }
  function collectExpandedGroupKeys(nodes, depth = 1, maxExpandedDepth = 1) {
    if (!Array.isArray(nodes) || depth > maxExpandedDepth) {
      return []
    }
    return nodes.flatMap((node) => {
      const currentId = node?.id !== undefined && node?.id !== null ? [node.id] : []
      return [
        ...currentId,
        ...collectExpandedGroupKeys(node?.children || [], depth + 1, maxExpandedDepth)
      ]
    })
  }
  function normalizeOptionText(value) {
    return String(value ?? '').trim()
  }
  function buildOption(value, label, extra = {}) {
    return {
      value,
      label,
      ...extra
    }
  }
  async function loadEnabledFieldDefinitions() {
    const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
      timeoutMessage: t('pages.basicInfo.whMat.messages.enabledFieldsTimeout')
    })
    enabledFields.value = normalizeWhMatEnabledFields(fields)
    enabledFields.value.forEach((field) => {
      const dynamicKey = getWhMatDynamicFieldKey(field.fields)
      if (searchForm.value[dynamicKey] === undefined) {
        searchForm.value[dynamicKey] = ''
      }
    })
    resetColumns()
  }
  function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
@@ -237,6 +741,99 @@
    }
  }
  async function loadSerialRuleOptions() {
    try {
      const response = await guardRequestWithMessage(
        fetchSerialRulePage({ current: 1, pageSize: 200 }),
        { records: [] },
        { timeoutMessage: t('pages.basicInfo.whMat.messages.serialRuleTimeout') }
      )
      serialRuleOptions.value = resolveWhMatSerialRuleOptions(
        defaultResponseAdapter(response).records
      )
    } catch (error) {
      serialRuleOptions.value = []
      ElMessage.error(error?.message || t('pages.basicInfo.whMat.messages.serialRuleLoadFailed'))
    }
  }
  async function ensureBindLocOptionsLoaded(force = false) {
    if (
      !force &&
      areaOptions.value.length &&
      areaMatOptions.value.length &&
      locOptions.value.length
    ) {
      return
    }
    if (bindLocOptionsLoading.value) {
      return
    }
    bindLocOptionsLoading.value = true
    try {
      const [areasResponse, areaMatResponse, locResponse] = await Promise.all([
        guardRequestWithMessage(fetchWarehouseAreasList(), [], {
          timeoutMessage: t('pages.basicInfo.whMat.messages.bindLocTimeout')
        }),
        guardRequestWithMessage(fetchLocAreaMatList(), [], {
          timeoutMessage: t('pages.basicInfo.whMat.messages.bindLocTimeout')
        }),
        guardRequestWithMessage(
          fetchLocPage({ current: 1, pageSize: 1000 }),
          { records: [] },
          {
            timeoutMessage: t('pages.basicInfo.whMat.messages.bindLocTimeout')
          }
        )
      ])
      areaOptions.value = defaultResponseAdapter(areasResponse)
        .records.map((item) =>
          buildOption(
            Number(item.id),
            [normalizeOptionText(item.name), normalizeOptionText(item.code)]
              .filter(Boolean)
              .join(' · ') || t('common.placeholder.empty'),
            {
              areaId: Number(item.id),
              warehouseId: item.warehouseId !== undefined ? Number(item.warehouseId) : void 0
            }
          )
        )
        .filter((item) => Number.isFinite(item.value))
      areaMatOptions.value = defaultResponseAdapter(areaMatResponse)
        .records.map((item) =>
          buildOption(
            Number(item.id),
            [normalizeOptionText(item.code), normalizeOptionText(item.depict || item.name)]
              .filter(Boolean)
              .join(' · ') || t('common.placeholder.empty'),
            {
              areaMatId: Number(item.id),
              areaId: item.areaId !== undefined ? Number(item.areaId) : void 0
            }
          )
        )
        .filter((item) => Number.isFinite(item.value))
      locOptions.value = defaultResponseAdapter(locResponse)
        .records.map((item) =>
          buildOption(
            Number(item.id),
            normalizeOptionText(item.code) || t('common.placeholder.empty'),
            {
              areaId: item.areaId !== undefined ? Number(item.areaId) : void 0
            }
          )
        )
        .filter((item) => Number.isFinite(item.value))
    } finally {
      bindLocOptionsLoading.value = false
    }
  }
  async function loadMatnrList() {
    loading.value = true
    try {
@@ -244,7 +841,7 @@
        fetchMatnrPage(
          buildMatnrPageQueryParams({
            ...searchForm.value,
            groupId: selectedGroupId.value,
            groupId: searchForm.value?.groupId || selectedGroupId.value,
            current: pagination.current,
            pageSize: pagination.size
          })
@@ -258,7 +855,7 @@
        { timeoutMessage: t('pages.basicInfo.whMat.messages.listTimeout') }
      )
      tableData.value = Array.isArray(response?.records)
        ? response.records.map((record) => normalizeMatnrRow(record, t))
        ? response.records.map((record) => normalizeMatnrRow(record, t, enabledFields.value))
        : []
      updatePaginationState(pagination, response, pagination.current, pagination.size)
    } catch (error) {
@@ -269,16 +866,21 @@
    }
  }
  async function loadMatnrDetail(id) {
    return await guardRequestWithMessage(
      fetchMatnrDetail(id),
      {},
      {
        timeoutMessage: t('pages.basicInfo.whMat.messages.detailTimeout')
      }
    )
  }
  async function openDetailDrawer(row) {
    detailDrawerVisible.value = true
    detailLoading.value = true
    try {
      detailData.value = normalizeMatnrDetail(
        await guardRequestWithMessage(fetchMatnrDetail(row.id), {}, {
          timeoutMessage: t('pages.basicInfo.whMat.messages.detailTimeout')
        }),
        t
      )
      detailData.value = normalizeMatnrDetail(await loadMatnrDetail(row.id), t, enabledFields.value)
    } catch (error) {
      detailDrawerVisible.value = false
      detailData.value = {}
@@ -288,21 +890,236 @@
    }
  }
  async function openEditDialog(row) {
    try {
      const detail = await loadMatnrDetail(row.id)
      showDialog('edit', detail)
    } catch (error) {
      ElMessage.error(error?.message || t('pages.basicInfo.whMat.messages.detailLoadFailed'))
    }
  }
  const {
    dialogVisible,
    dialogType,
    currentRecord: currentMaterialData,
    selectedRows,
    handleSelectionChange,
    showDialog,
    handleDialogSubmit,
    handleDelete,
    handleBatchDelete
  } = useCrudPage({
    createEmptyModel: () =>
      buildWhMatDialogModel({ groupId: selectedGroupId.value || searchForm.value?.groupId || '' }),
    buildEditModel: (record) => buildWhMatDialogModel(record),
    buildSavePayload: (formData) => buildWhMatSavePayload(formData),
    saveRequest: fetchSaveMatnr,
    updateRequest: fetchUpdateMatnr,
    deleteRequest: fetchDeleteMatnr,
    entityName: t('pages.basicInfo.whMat.entity'),
    resolveRecordLabel: (record) => record?.name || record?.code || record?.id,
    refreshCreate: loadMatnrList,
    refreshUpdate: loadMatnrList,
    refreshRemove: loadMatnrList
  })
  handleDeleteAction = handleDelete
  const getSelectedIds = () =>
    selectedRows.value.map((item) => Number(item?.id)).filter((id) => Number.isFinite(id))
  const ensureSelectedRows = () => {
    const ids = getSelectedIds()
    if (!ids.length) {
      ElMessage.warning(t('pages.basicInfo.whMat.messages.selectAtLeastOne'))
      return []
    }
    return ids
  }
  function openBatchDialog(type) {
    if (!ensureSelectedRows().length) {
      return
    }
    batchDialogType.value = type
    batchDialogVisible.value = true
  }
  function openBatchGroupDialog() {
    if (!ensureSelectedRows().length) {
      return
    }
    batchGroupDialogVisible.value = true
  }
  async function openBindLocDialog() {
    if (!ensureSelectedRows().length) {
      return
    }
    try {
      await ensureBindLocOptionsLoaded()
      bindLocDialogVisible.value = true
    } catch (error) {
      ElMessage.error(error?.message || t('pages.basicInfo.whMat.messages.bindLocLoadFailed'))
    }
  }
  async function handleBatchDialogSubmit(formData) {
    const ids = ensureSelectedRows()
    if (!ids.length) {
      batchDialogVisible.value = false
      return
    }
    try {
      await fetchBatchUpdateMatnr({
        ids,
        matnr: formData
      })
      ElMessage.success(t('crud.messages.updateSuccess'))
      batchDialogVisible.value = false
      selectedRows.value = []
      await loadMatnrList()
    } catch (error) {
      ElMessage.error(error?.message || t('crud.messages.submitFailed'))
    }
  }
  async function handleBatchGroupSubmit(formData) {
    const ids = ensureSelectedRows()
    if (!ids.length) {
      batchGroupDialogVisible.value = false
      return
    }
    try {
      await fetchBindMatnrGroup({
        ids,
        groupId: formData.groupId
      })
      ElMessage.success(t('crud.messages.updateSuccess'))
      batchGroupDialogVisible.value = false
      selectedRows.value = []
      await loadMatnrList()
    } catch (error) {
      ElMessage.error(error?.message || t('crud.messages.submitFailed'))
    }
  }
  async function handleBindLocSubmit(formData) {
    const ids = ensureSelectedRows()
    if (!ids.length) {
      bindLocDialogVisible.value = false
      return
    }
    try {
      await fetchBindLocAreaMatRelaByMatnr({
        ...formData,
        matnrId: ids
      })
      ElMessage.success(t('crud.messages.updateSuccess'))
      bindLocDialogVisible.value = false
      selectedRows.value = []
      await loadMatnrList()
    } catch (error) {
      ElMessage.error(error?.message || t('crud.messages.submitFailed'))
    }
  }
  const buildPreviewMeta = (rows) => {
    const now = new Date()
    return {
      reportDate: now.toLocaleDateString('zh-CN'),
      printedAt: now.toLocaleString('zh-CN', { hour12: false }),
      operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
      count: rows.length,
      reportStyle: { ...WH_MAT_REPORT_STYLE }
    }
  }
  const resolvePrintRecords = async (payload) => {
    if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
      return defaultResponseAdapter(await fetchGetMatnrMany(payload.ids)).records
    }
    return tableData.value
  }
  const {
    previewVisible,
    previewRows,
    previewMeta,
    handlePreviewVisibleChange,
    handleExport,
    handlePrint
  } = usePrintExportPage({
    downloadFileName: 'matnr.xlsx',
    requestExport: (payload) =>
      fetchExportMatnrReport(payload, {
        headers: {
          Authorization: userStore.accessToken || ''
        }
      }),
    resolvePrintRecords,
    buildPreviewRows: (records) => buildWhMatPrintRows(records, t),
    buildPreviewMeta
  })
  const resolvedPreviewMeta = computed(() =>
    buildWhMatReportMeta({
      previewMeta: previewMeta.value,
      count: previewRows.value.length,
      orientation: previewMeta.value?.reportStyle?.orientation || WH_MAT_REPORT_STYLE.orientation
    })
  )
  async function downloadFile(response, fallbackName) {
    if (!response?.ok) {
      throw new Error(
        t('crud.messages.exportFailedWithStatus', { status: response?.status || '-' })
      )
    }
    const blob = await response.blob()
    const downloadUrl = window.URL.createObjectURL(blob)
    const link = document.createElement('a')
    link.href = downloadUrl
    link.download = fallbackName
    document.body.appendChild(link)
    link.click()
    link.remove()
    window.URL.revokeObjectURL(downloadUrl)
  }
  function handleShowDialog(type) {
    showDialog(type)
  }
  function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params
      ...params,
      orderBy: searchForm.value?.orderBy || 'create_time desc'
    }
    pagination.current = 1
    if (searchForm.value.groupId) {
      selectedGroupId.value = null
    }
    loadMatnrList()
  }
  async function handleReset() {
    searchForm.value = createWhMatSearchState()
    enabledFields.value.forEach((field) => {
      searchForm.value[getWhMatDynamicFieldKey(field.fields)] = ''
    })
    pagination.current = 1
    selectedGroupId.value = null
    groupSearch.value = ''
    await Promise.all([loadGroupTree(), loadMatnrList()])
  }
  function handleRefresh() {
    loadMatnrList()
  }
  function handleSizeChange(size) {
@@ -318,6 +1135,7 @@
  function handleGroupNodeClick(data) {
    selectedGroupId.value = data?.id ?? null
    delete searchForm.value.groupId
    pagination.current = 1
    loadMatnrList()
  }
@@ -335,46 +1153,154 @@
    await Promise.all([loadGroupTree(), loadMatnrList()])
  }
  async function handleImportFileChange(uploadFile) {
    if (!uploadFile?.raw) {
      return
    }
    importing.value = true
    try {
      await fetchImportMatnr(uploadFile.raw)
      ElMessage.success(t('pages.basicInfo.whMat.messages.importSuccess'))
      await loadMatnrList()
    } catch (error) {
      ElMessage.error(error?.message || t('pages.basicInfo.whMat.messages.importFailed'))
    } finally {
      importing.value = false
    }
  }
  async function handleDownloadTemplate() {
    templateDownloading.value = true
    try {
      const response = await fetchDownloadMatnrTemplate(
        {},
        {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }
      )
      await downloadFile(response, 'matnr-template.xlsx')
      ElMessage.success(t('pages.basicInfo.whMat.messages.templateDownloadSuccess'))
    } catch (error) {
      ElMessage.error(error?.message || t('pages.basicInfo.whMat.messages.templateDownloadFailed'))
    } finally {
      templateDownloading.value = false
    }
  }
  onMounted(async () => {
    await Promise.all([loadGroupTree(), loadMatnrList()])
    await Promise.allSettled([
      loadEnabledFieldDefinitions(),
      loadGroupTree(),
      loadSerialRuleOptions()
    ])
    await loadMatnrList()
  })
</script>
<style scoped>
  .wh-mat-page-root {
    height: 100%;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  .wh-mat-page {
    display: flex;
    flex-direction: row;
    align-items: flex-start;
    align-items: stretch;
    gap: 16px;
    flex: 1 1 auto;
    min-height: 0;
  }
  .wh-mat-page__sidebar {
    width: 320px;
    flex: 0 0 320px;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  .wh-mat-page__sidebar-card {
    position: sticky;
    top: 16px;
    height: 100%;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .wh-mat-page__sidebar-card :deep(.el-card__body) {
    height: 100%;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .wh-mat-page__tree-scroll {
    height: calc(100vh - 320px);
    min-height: 420px;
    flex: 1 1 auto;
    min-height: 0;
  }
  .wh-mat-page__content {
    height: 100%;
    min-width: 0;
    min-height: 0;
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .wh-mat-page__content > * + * {
    margin-top: 16px;
  }
  .wh-mat-page__content > :deep(.art-search-bar) {
    flex: 0 0 auto;
  }
  .wh-mat-page__content > :deep(.art-table-card) {
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
  }
  .wh-mat-page__content > :deep(.art-table-card .el-card__body) {
    display: flex;
    min-height: 0;
    flex-direction: column;
  }
  .wh-mat-page__content > :deep(.art-table-card #art-table-header) {
    flex: 0 0 auto;
  }
  .wh-mat-page__content > :deep(.art-table-card .art-table) {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  .wh-mat-page__content > :deep(.art-table-card .art-table .el-table) {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
  }
  .wh-mat-page__content > :deep(.art-table-card .art-table .pagination) {
    flex: 0 0 auto;
  }
  @media (max-width: 1024px) {
    .wh-mat-page {
      flex-direction: column;
      flex: none;
    }
    .wh-mat-page__sidebar {
@@ -383,7 +1309,7 @@
    }
    .wh-mat-page__sidebar-card {
      position: static;
      height: auto;
    }
    .wh-mat-page__tree-scroll {
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-batch-dialog.vue
New file
@@ -0,0 +1,232 @@
<template>
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    width="640px"
    align-center
    destroy-on-close
    @update:model-value="handleCancel"
    @closed="handleClosed"
  >
    <ArtForm
      ref="formRef"
      v-model="form"
      :items="formItems"
      :rules="rules"
      :span="12"
      :gutter="20"
      label-width="120px"
      :show-reset="false"
      :show-submit="false"
    />
    <template #footer>
      <span class="dialog-footer">
        <ElButton @click="handleCancel">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" @click="handleSubmit">{{ t('common.confirm') }}</ElButton>
      </span>
    </template>
  </ElDialog>
</template>
<script setup>
  import { computed, nextTick, reactive, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import {
    getWhMatFlagCheckOptions,
    getWhMatStatusOptions,
    getWhMatStockLevelOptions
  } from '../whMatPage.helpers'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    actionType: { type: String, default: 'status' }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const { t } = useI18n()
  const formRef = ref()
  const form = reactive(createFormState())
  function createFormState() {
    return {
      status: '',
      stockLevel: '',
      validWarn: null,
      valid: null,
      flagCheck: ''
    }
  }
  const dialogTitle = computed(() =>
    t(`pages.basicInfo.whMat.batchDialog.titles.${props.actionType}`)
  )
  const formItems = computed(() => {
    if (props.actionType === 'status') {
      return [
        {
          label: t('table.status'),
          key: 'status',
          type: 'select',
          span: 24,
          props: {
            clearable: true,
            placeholder: t('pages.basicInfo.whMat.search.statusPlaceholder'),
            options: getWhMatStatusOptions(t)
          }
        }
      ]
    }
    if (props.actionType === 'stockLevel') {
      return [
        {
          label: t('pages.basicInfo.whMat.batchDialog.fields.stockLevel'),
          key: 'stockLevel',
          type: 'select',
          span: 24,
          props: {
            clearable: true,
            placeholder: t('pages.basicInfo.whMat.batchDialog.placeholders.stockLevel'),
            options: getWhMatStockLevelOptions()
          }
        }
      ]
    }
    if (props.actionType === 'validWarn') {
      return [
        {
          label: t('pages.basicInfo.whMat.dialog.fields.validWarn'),
          key: 'validWarn',
          type: 'number',
          props: {
            min: 0,
            controlsPosition: 'right',
            valueOnClear: null,
            placeholder: t('pages.basicInfo.whMat.batchDialog.placeholders.validWarn')
          }
        },
        {
          label: t('pages.basicInfo.whMat.dialog.fields.valid'),
          key: 'valid',
          type: 'number',
          props: {
            min: 0,
            controlsPosition: 'right',
            valueOnClear: null,
            placeholder: t('pages.basicInfo.whMat.batchDialog.placeholders.valid')
          }
        }
      ]
    }
    return [
      {
        label: t('pages.basicInfo.whMat.dialog.fields.flagCheck'),
        key: 'flagCheck',
        type: 'select',
        span: 24,
        props: {
          clearable: true,
          placeholder: t('pages.basicInfo.whMat.batchDialog.placeholders.flagCheck'),
          options: getWhMatFlagCheckOptions(t)
        }
      }
    ]
  })
  const rules = computed(() => {
    if (props.actionType === 'status') {
      return {
        status: [
          {
            required: true,
            message: t('pages.basicInfo.whMat.batchDialog.validation.status'),
            trigger: 'change'
          }
        ]
      }
    }
    if (props.actionType === 'stockLevel') {
      return {
        stockLevel: [
          {
            required: true,
            message: t('pages.basicInfo.whMat.batchDialog.validation.stockLevel'),
            trigger: 'change'
          }
        ]
      }
    }
    if (props.actionType === 'validWarn') {
      return {
        validWarn: [
          {
            required: true,
            message: t('pages.basicInfo.whMat.batchDialog.validation.validWarn'),
            trigger: 'blur'
          }
        ],
        valid: [
          {
            required: true,
            message: t('pages.basicInfo.whMat.batchDialog.validation.valid'),
            trigger: 'blur'
          }
        ]
      }
    }
    return {
      flagCheck: [
        {
          required: true,
          message: t('pages.basicInfo.whMat.batchDialog.validation.flagCheck'),
          trigger: 'change'
        }
      ]
    }
  })
  function resetForm() {
    Object.assign(form, createFormState())
    formRef.value?.clearValidate?.()
  }
  async function handleSubmit() {
    if (!formRef.value) return
    try {
      await formRef.value.validate()
      emit('submit', { ...form })
    } catch {
      return
    }
  }
  function handleCancel() {
    emit('update:visible', false)
  }
  function handleClosed() {
    resetForm()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (visible) {
        resetForm()
        nextTick(() => {
          formRef.value?.clearValidate?.()
        })
      }
    },
    { immediate: true }
  )
</script>
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-batch-group-dialog.vue
New file
@@ -0,0 +1,113 @@
<template>
  <ElDialog
    :title="t('pages.basicInfo.whMat.batchGroupDialog.title')"
    :model-value="visible"
    width="640px"
    align-center
    destroy-on-close
    @update:model-value="handleCancel"
    @closed="handleClosed"
  >
    <ArtForm
      ref="formRef"
      v-model="form"
      :items="formItems"
      :rules="rules"
      :span="24"
      :gutter="20"
      label-width="120px"
      :show-reset="false"
      :show-submit="false"
    />
    <template #footer>
      <span class="dialog-footer">
        <ElButton @click="handleCancel">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" @click="handleSubmit">{{ t('common.confirm') }}</ElButton>
      </span>
    </template>
  </ElDialog>
</template>
<script setup>
  import { computed, nextTick, reactive, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    groupOptions: { type: Array, default: () => [] }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const { t } = useI18n()
  const formRef = ref()
  const form = reactive({ groupId: '' })
  const formItems = computed(() => [
    {
      label: t('pages.basicInfo.whMat.dialog.fields.groupId'),
      key: 'groupId',
      type: 'treeselect',
      props: {
        data: props.groupOptions,
        props: {
          label: 'displayLabel',
          value: 'value',
          children: 'children'
        },
        placeholder: t('pages.basicInfo.whMat.dialog.placeholders.groupId'),
        clearable: false,
        checkStrictly: true,
        defaultExpandAll: true
      }
    }
  ])
  const rules = computed(() => ({
    groupId: [
      {
        required: true,
        message: t('pages.basicInfo.whMat.dialog.validation.groupId'),
        trigger: 'change'
      }
    ]
  }))
  function resetForm() {
    form.groupId = ''
    formRef.value?.clearValidate?.()
  }
  async function handleSubmit() {
    if (!formRef.value) return
    try {
      await formRef.value.validate()
      emit('submit', { ...form })
    } catch {
      return
    }
  }
  function handleCancel() {
    emit('update:visible', false)
  }
  function handleClosed() {
    resetForm()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (visible) {
        resetForm()
        nextTick(() => {
          formRef.value?.clearValidate?.()
        })
      }
    },
    { immediate: true }
  )
</script>
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-bind-loc-dialog.vue
New file
@@ -0,0 +1,184 @@
<template>
  <ElDialog
    :title="t('pages.basicInfo.whMat.bindLocDialog.title')"
    :model-value="visible"
    width="960px"
    align-center
    destroy-on-close
    @update:model-value="handleCancel"
    @closed="handleClosed"
  >
    <ArtForm
      ref="formRef"
      v-model="form"
      :items="formItems"
      :rules="rules"
      :span="12"
      :gutter="20"
      label-width="120px"
      :show-reset="false"
      :show-submit="false"
    />
    <template #footer>
      <span class="dialog-footer">
        <ElButton @click="handleCancel">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" @click="handleSubmit">{{ t('common.confirm') }}</ElButton>
      </span>
    </template>
  </ElDialog>
</template>
<script setup>
  import { computed, nextTick, reactive, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    areaMatOptions: { type: Array, default: () => [] },
    areaOptions: { type: Array, default: () => [] },
    locOptions: { type: Array, default: () => [] }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const { t } = useI18n()
  const formRef = ref()
  const form = reactive({
    areaMatId: '',
    areaId: '',
    locId: []
  })
  const filteredAreaMatOptions = computed(() => {
    const selectedAreaId =
      form.areaId !== undefined && form.areaId !== null && form.areaId !== ''
        ? Number(form.areaId)
        : void 0
    if (selectedAreaId === void 0) {
      return props.areaMatOptions
    }
    return props.areaMatOptions.filter(
      (item) => item.areaId === void 0 || Number(item.areaId) === selectedAreaId
    )
  })
  const filteredLocOptions = computed(() => {
    const selectedAreaId =
      form.areaId !== undefined && form.areaId !== null && form.areaId !== ''
        ? Number(form.areaId)
        : void 0
    if (selectedAreaId === void 0) {
      return props.locOptions
    }
    return props.locOptions.filter(
      (item) => item.areaId === void 0 || Number(item.areaId) === selectedAreaId
    )
  })
  const formItems = computed(() => [
    {
      label: t('pages.basicInfo.whMat.bindLocDialog.fields.areaMatId'),
      key: 'areaMatId',
      type: 'select',
      props: {
        clearable: true,
        filterable: true,
        placeholder: t('pages.basicInfo.whMat.bindLocDialog.placeholders.areaMatId'),
        options: filteredAreaMatOptions.value
      }
    },
    {
      label: t('pages.basicInfo.whMat.bindLocDialog.fields.areaId'),
      key: 'areaId',
      type: 'select',
      props: {
        clearable: true,
        filterable: true,
        placeholder: t('pages.basicInfo.whMat.bindLocDialog.placeholders.areaId'),
        options: props.areaOptions
      }
    },
    {
      label: t('pages.basicInfo.whMat.bindLocDialog.fields.locId'),
      key: 'locId',
      type: 'select',
      span: 24,
      props: {
        clearable: true,
        filterable: true,
        multiple: true,
        collapseTags: true,
        placeholder: t('pages.basicInfo.whMat.bindLocDialog.placeholders.locId'),
        options: filteredLocOptions.value
      }
    }
  ])
  const rules = computed(() => ({
    areaMatId: [
      {
        required: true,
        message: t('pages.basicInfo.whMat.bindLocDialog.validation.areaMatId'),
        trigger: 'change'
      }
    ],
    areaId: [
      {
        required: true,
        message: t('pages.basicInfo.whMat.bindLocDialog.validation.areaId'),
        trigger: 'change'
      }
    ],
    locId: [
      {
        required: true,
        message: t('pages.basicInfo.whMat.bindLocDialog.validation.locId'),
        trigger: 'change'
      }
    ]
  }))
  function resetForm() {
    form.areaMatId = ''
    form.areaId = ''
    form.locId = []
    formRef.value?.clearValidate?.()
  }
  async function handleSubmit() {
    if (!formRef.value) return
    try {
      await formRef.value.validate()
      emit('submit', {
        areaMatId: form.areaMatId,
        areaId: form.areaId,
        locId: Array.isArray(form.locId) ? [...form.locId] : []
      })
    } catch {
      return
    }
  }
  function handleCancel() {
    emit('update:visible', false)
  }
  function handleClosed() {
    resetForm()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (visible) {
        resetForm()
        nextTick(() => {
          formRef.value?.clearValidate?.()
        })
      }
    },
    { immediate: true }
  )
</script>
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-dialog.vue
New file
@@ -0,0 +1,380 @@
<template>
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    width="960px"
    align-center
    destroy-on-close
    @update:model-value="handleCancel"
    @closed="handleClosed"
  >
    <ElForm
      ref="formRef"
      :model="form"
      :rules="rules"
      label-width="110px"
      class="wh-mat-dialog-form"
    >
      <ElTabs v-model="activeTab">
        <ElTabPane :label="t('pages.basicInfo.whMat.dialog.tabs.basic')" name="basic">
          <ElRow :gutter="20">
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.code')" prop="code">
                <ElInput
                  v-model.trim="form.code"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.code')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.name')" prop="name">
                <ElInput
                  v-model.trim="form.name"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.name')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.groupId')" prop="groupId">
                <ElTreeSelect
                  v-model="form.groupId"
                  :data="groupOptions"
                  :props="groupTreeProps"
                  check-strictly
                  default-expand-all
                  clearable
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.groupId')"
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem
                :label="t('pages.basicInfo.whMat.dialog.fields.useOrgName')"
                prop="useOrgName"
              >
                <ElInput
                  v-model.trim="form.useOrgName"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.useOrgName')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.spec')" prop="spec">
                <ElInput
                  v-model.trim="form.spec"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.spec')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.model')" prop="model">
                <ElInput
                  v-model.trim="form.model"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.model')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.color')" prop="color">
                <ElInput
                  v-model.trim="form.color"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.color')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.size')" prop="size">
                <ElInput
                  v-model.trim="form.size"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.size')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.weight')" prop="weight">
                <ElInputNumber
                  v-model="form.weight"
                  :min="0"
                  controls-position="right"
                  class="w-full"
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.unit')" prop="unit">
                <ElInput
                  v-model.trim="form.unit"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.unit')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.purUnit')" prop="purUnit">
                <ElInput
                  v-model.trim="form.purUnit"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.purUnit')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem
                :label="t('pages.basicInfo.whMat.dialog.fields.describle')"
                prop="describle"
              >
                <ElInput
                  v-model.trim="form.describle"
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.describle')"
                  clearable
                />
              </ElFormItem>
            </ElCol>
          </ElRow>
        </ElTabPane>
        <ElTabPane :label="t('pages.basicInfo.whMat.dialog.tabs.control')" name="control">
          <ElRow :gutter="20">
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.safeQty')" prop="safeQty">
                <ElInputNumber
                  v-model="form.safeQty"
                  :min="0"
                  controls-position="right"
                  class="w-full"
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.minQty')" prop="minQty">
                <ElInputNumber
                  v-model="form.minQty"
                  :min="0"
                  controls-position="right"
                  class="w-full"
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.maxQty')" prop="maxQty">
                <ElInputNumber
                  v-model="form.maxQty"
                  :min="0"
                  controls-position="right"
                  class="w-full"
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.stagn')" prop="stagn">
                <ElInputNumber
                  v-model="form.stagn"
                  :min="0"
                  controls-position="right"
                  class="w-full"
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.valid')" prop="valid">
                <ElInputNumber
                  v-model="form.valid"
                  :min="0"
                  controls-position="right"
                  class="w-full"
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem
                :label="t('pages.basicInfo.whMat.dialog.fields.validWarn')"
                prop="validWarn"
              >
                <ElInputNumber
                  v-model="form.validWarn"
                  :min="0"
                  controls-position="right"
                  class="w-full"
                />
              </ElFormItem>
            </ElCol>
            <ElCol :span="12">
              <ElFormItem
                :label="t('pages.basicInfo.whMat.dialog.fields.flagCheck')"
                prop="flagCheck"
              >
                <ElSelect
                  v-model="form.flagCheck"
                  clearable
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.flagCheck')"
                >
                  <ElOption
                    v-for="option in flagCheckOptions"
                    :key="option.value"
                    :label="option.label"
                    :value="option.value"
                  />
                </ElSelect>
              </ElFormItem>
            </ElCol>
          </ElRow>
        </ElTabPane>
        <ElTabPane :label="t('pages.basicInfo.whMat.dialog.tabs.batchRule')" name="batchRule">
          <ElRow :gutter="20">
            <ElCol :span="12">
              <ElFormItem :label="t('pages.basicInfo.whMat.dialog.fields.rglarId')" prop="rglarId">
                <ElSelect
                  v-model="form.rglarId"
                  clearable
                  filterable
                  :placeholder="t('pages.basicInfo.whMat.dialog.placeholders.rglarId')"
                >
                  <ElOption
                    v-for="option in serialRuleOptions"
                    :key="option.value"
                    :label="option.label"
                    :value="option.value"
                  />
                </ElSelect>
              </ElFormItem>
            </ElCol>
          </ElRow>
        </ElTabPane>
      </ElTabs>
    </ElForm>
    <template #footer>
      <span class="dialog-footer">
        <ElButton @click="handleCancel">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" @click="handleSubmit">{{ t('common.confirm') }}</ElButton>
      </span>
    </template>
  </ElDialog>
</template>
<script setup>
  import { computed, nextTick, reactive, ref, watch } from 'vue'
  import { useI18n } from 'vue-i18n'
  import {
    buildWhMatDialogModel,
    createWhMatFormState,
    getWhMatFlagCheckOptions
  } from '../whMatPage.helpers'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    dialogType: { type: String, default: 'add' },
    materialData: { type: Object, default: () => ({}) },
    groupOptions: { type: Array, default: () => [] },
    serialRuleOptions: { type: Array, default: () => [] }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const { t } = useI18n()
  const formRef = ref()
  const activeTab = ref('basic')
  const form = reactive(createWhMatFormState())
  const dialogTitle = computed(() =>
    props.dialogType === 'edit'
      ? t('pages.basicInfo.whMat.dialog.titleEdit')
      : t('pages.basicInfo.whMat.dialog.titleCreate')
  )
  const flagCheckOptions = computed(() => getWhMatFlagCheckOptions(t))
  const groupTreeProps = {
    label: 'displayLabel',
    value: 'value',
    children: 'children'
  }
  const rules = computed(() => ({
    code: [
      {
        required: true,
        message: t('pages.basicInfo.whMat.dialog.validation.code'),
        trigger: 'blur'
      }
    ],
    name: [
      {
        required: true,
        message: t('pages.basicInfo.whMat.dialog.validation.name'),
        trigger: 'blur'
      }
    ],
    groupId: [
      {
        required: true,
        message: t('pages.basicInfo.whMat.dialog.validation.groupId'),
        trigger: 'change'
      }
    ]
  }))
  function loadFormData() {
    Object.assign(form, buildWhMatDialogModel(props.materialData))
    activeTab.value = 'basic'
  }
  function resetForm() {
    Object.assign(form, createWhMatFormState())
    formRef.value?.clearValidate?.()
    activeTab.value = 'basic'
  }
  async function handleSubmit() {
    try {
      await formRef.value?.validate?.()
      emit('submit', { ...form })
    } catch {
      return
    }
  }
  function handleCancel() {
    emit('update:visible', false)
  }
  function handleClosed() {
    resetForm()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (visible) {
        loadFormData()
        nextTick(() => {
          formRef.value?.clearValidate?.()
        })
      }
    },
    { immediate: true }
  )
  watch(
    () => props.materialData,
    () => {
      if (props.visible) {
        loadFormData()
      }
    },
    { deep: true }
  )
</script>
<style scoped>
  .wh-mat-dialog-form :deep(.el-select),
  .wh-mat-dialog-form :deep(.el-tree-select) {
    width: 100%;
  }
</style>
rsf-design/src/views/basic-info/wh-mat/whMatPage.helpers.js
@@ -1,5 +1,13 @@
import { $t } from '@/locales'
export const WH_MAT_REPORT_TITLE = '物料报表'
export const WH_MAT_REPORT_STYLE = {
  orientation: 'landscape',
  titleAlign: 'center',
  titleLevel: 'h2'
}
export const WH_MAT_DYNAMIC_FIELD_PREFIX = 'extendField__'
function normalizeText(value) {
  return String(value ?? '').trim()
}
@@ -20,33 +28,137 @@
  return Number.isFinite(numericValue) ? numericValue : null
}
function normalizeNullableInteger(value) {
  const numericValue = normalizeNullableNumber(value)
  return numericValue === null ? null : Math.trunc(numericValue)
}
function normalizeBooleanLikeText(value, t = $t) {
  if (value === 1 || value === '1') return t('common.status.yes')
  if (value === 0 || value === '0') return t('common.status.no')
  return t('common.placeholder.empty')
}
export function createWhMatSearchState() {
  return {
    condition: '',
    code: '',
    name: '',
    platCode: '',
    spec: '',
    model: '',
    barcode: ''
    color: '',
    size: '',
    unit: '',
    purUnit: '',
    stockUnit: '',
    barcode: '',
    describle: '',
    groupId: '',
    rglarId: '',
    weight: null,
    nromNum: null,
    stockLevel: '',
    flagLabelMange: '',
    safeQty: null,
    minQty: null,
    maxQty: null,
    stagn: null,
    valid: null,
    status: '',
    flagCheck: '',
    validWarn: null,
    memo: '',
    orderBy: 'create_time desc'
  }
}
export function createWhMatFormState() {
  return {
    id: void 0,
    code: '',
    name: '',
    groupId: '',
    useOrgName: '',
    spec: '',
    model: '',
    color: '',
    size: '',
    weight: void 0,
    unit: '',
    purUnit: '',
    describle: '',
    safeQty: void 0,
    minQty: void 0,
    maxQty: void 0,
    stagn: void 0,
    valid: void 0,
    validWarn: void 0,
    flagCheck: 0,
    rglarId: ''
  }
}
export function buildWhMatPageQueryParams(params = {}) {
  const result = {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20
    pageSize: params.pageSize || params.size || 20,
    orderBy: normalizeText(params.orderBy) || 'create_time desc'
  }
  ;['condition', 'code', 'name', 'spec', 'model', 'barcode'].forEach((key) => {
  ;[
    'condition',
    'code',
    'name',
    'platCode',
    'spec',
    'model',
    'color',
    'size',
    'unit',
    'purUnit',
    'stockUnit',
    'barcode',
    'describle',
    'memo'
  ].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) {
      result[key] = value
    }
  })
  ;[
    'groupId',
    'rglarId',
    'weight',
    'nromNum',
    'stockLevel',
    'flagLabelMange',
    'safeQty',
    'minQty',
    'maxQty',
    'stagn',
    'valid',
    'status',
    'flagCheck',
    'validWarn'
  ].forEach((key) => {
    const value = params[key]
    if (value !== '' && value !== null && value !== undefined) {
      const numericValue = Number(value)
      result[key] = Number.isFinite(numericValue) ? numericValue : value
    }
  })
  if (params.groupId !== undefined && params.groupId !== null && params.groupId !== '') {
    result.groupId = String(params.groupId)
  }
  Object.entries(params).forEach(([key, value]) => {
    if (!key.startsWith(WH_MAT_DYNAMIC_FIELD_PREFIX)) {
      return
    }
    const normalizedValue = normalizeText(value)
    if (normalizedValue) {
      result[key.slice(WH_MAT_DYNAMIC_FIELD_PREFIX.length)] = normalizedValue
    }
  })
  return result
}
@@ -63,7 +175,7 @@
  }
  return records.map((item) => {
    const children = normalizeWhMatGroupTreeRows(item?.children || [])
    const children = normalizeWhMatGroupTreeRows(item?.children || [], t)
    const id = normalizeNullableNumber(item?.id)
    const code = normalizeText(item?.code)
    const name = normalizeText(item?.name)
@@ -77,8 +189,12 @@
      name,
      label,
      displayLabel: label,
      value: id,
      status: normalizeNullableNumber(item?.status),
      statusText: normalizeNumber(item?.status, 1) === 1 ? t('common.status.normal') : t('common.status.frozen'),
      statusText:
        normalizeNumber(item?.status, 1) === 1
          ? t('common.status.normal')
          : t('common.status.frozen'),
      statusType: normalizeNumber(item?.status, 1) === 1 ? 'success' : 'danger',
      memo: normalizeText(item?.memo) || t('common.placeholder.empty'),
      children
@@ -86,62 +202,241 @@
  })
}
export function normalizeWhMatRow(record = {}, t = $t) {
  const statusValue = normalizeNullableNumber(record?.status)
export function resolveWhMatGroupOptions(treeRows = []) {
  if (!Array.isArray(treeRows)) {
    return []
  }
  return treeRows.map((item) => ({
    id: item.id,
    value: item.id,
    label:
      item.displayLabel || item.label || [item.name, item.code].filter(Boolean).join(' · ') || '-',
    displayLabel:
      item.displayLabel || item.label || [item.name, item.code].filter(Boolean).join(' · ') || '-',
    children: resolveWhMatGroupOptions(item.children || [])
  }))
}
export function resolveWhMatSerialRuleOptions(records = []) {
  if (!Array.isArray(records)) {
    return []
  }
  return records
    .map((item) => ({
      value: normalizeNullableNumber(item?.id),
      label: normalizeText(item?.name || item?.code || item?.description)
    }))
    .filter((item) => item.value !== null && item.label)
}
export function getWhMatStatusOptions(t = $t) {
  return [
    { value: 1, label: t('common.status.normal') },
    { value: 0, label: t('common.status.frozen') }
  ]
}
export function getWhMatFlagCheckOptions(t = $t) {
  return [
    { value: 0, label: t('common.status.no') },
    { value: 1, label: t('common.status.yes') }
  ]
}
export function getWhMatStockLevelOptions() {
  return [
    { value: 0, label: 'A' },
    { value: 1, label: 'B' },
    { value: 2, label: 'C' }
  ]
}
export function getWhMatFlagLabelManageOptions(t = $t) {
  return [
    { value: 0, label: t('common.status.no') },
    { value: 1, label: t('common.status.yes') }
  ]
}
export function getWhMatDynamicFieldKey(fieldName) {
  return `${WH_MAT_DYNAMIC_FIELD_PREFIX}${fieldName}`
}
export function normalizeWhMatEnabledFields(fields = []) {
  if (!Array.isArray(fields)) {
    return []
  }
  return fields
    .map((item) => ({
      fields: normalizeText(item?.fields),
      fieldsAlise: normalizeText(item?.fieldsAlise || item?.fieldsAlias || item?.fields)
    }))
    .filter((item) => item.fields)
}
export function attachWhMatDynamicFields(record = {}, enabledFields = []) {
  const extendFields =
    record?.extendFields &&
    typeof record.extendFields === 'object' &&
    !Array.isArray(record.extendFields)
      ? record.extendFields
      : {}
  const dynamicValues = {}
  enabledFields.forEach((field) => {
    dynamicValues[getWhMatDynamicFieldKey(field.fields)] = extendFields[field.fields] || ''
  })
  return {
    ...record,
    code: normalizeText(record?.code) || t('common.placeholder.empty'),
    name: normalizeText(record?.name) || t('common.placeholder.empty'),
    groupName: normalizeText(record?.groupId$ || record?.groupCode) || t('common.placeholder.empty'),
    shipperName: normalizeText(record?.shipperId$ || record?.shipperName) || t('common.placeholder.empty'),
    barcode: normalizeText(record?.barcode) || t('common.placeholder.empty'),
    spec: normalizeText(record?.spec) || t('common.placeholder.empty'),
    model: normalizeText(record?.model) || t('common.placeholder.empty'),
    color: normalizeText(record?.color) || t('common.placeholder.empty'),
    size: normalizeText(record?.size) || t('common.placeholder.empty'),
    unit: normalizeText(record?.unit) || t('common.placeholder.empty'),
    purUnit: normalizeText(record?.purUnit) || t('common.placeholder.empty'),
    stockUnit: normalizeText(record?.stockUnit) || t('common.placeholder.empty'),
    stockLevelText: normalizeText(record?.stockLeval$) || t('common.placeholder.empty'),
    flagLabelManageText: normalizeText(record?.flagLabelMange$) || t('common.placeholder.empty'),
    flagCheckText:
      record?.flagCheck === 1 || record?.flagCheck === '1'
        ? t('common.status.yes')
        : record?.flagCheck === 0 || record?.flagCheck === '0'
          ? t('common.status.no')
          : t('common.placeholder.empty'),
    statusText:
      normalizeText(record?.status$) ||
      (statusValue === 1
        ? t('common.status.normal')
        : statusValue === 0
          ? t('common.status.frozen')
          : t('common.placeholder.empty')),
    statusType: statusValue === 1 ? 'success' : statusValue === 0 ? 'danger' : 'info',
    safeQty: record?.safeQty ?? t('common.placeholder.empty'),
    minQty: record?.minQty ?? t('common.placeholder.empty'),
    maxQty: record?.maxQty ?? t('common.placeholder.empty'),
    valid: record?.valid ?? t('common.placeholder.empty'),
    validWarn: record?.validWarn ?? t('common.placeholder.empty'),
    stagn: record?.stagn ?? t('common.placeholder.empty'),
    describle: normalizeText(record?.describle) || t('common.placeholder.empty'),
    baseUnit: normalizeText(record?.baseUnit) || t('common.placeholder.empty'),
    useOrgName: normalizeText(record?.useOrgName) || t('common.placeholder.empty'),
    erpClsId: normalizeText(record?.erpClsId) || t('common.placeholder.empty'),
    memo: normalizeText(record?.memo) || t('common.placeholder.empty'),
    updateByText: normalizeText(record?.updateBy$) || t('common.placeholder.empty'),
    createByText: normalizeText(record?.createBy$) || t('common.placeholder.empty'),
    updateTimeText: normalizeText(record?.updateTime$ || record?.updateTime) || t('common.placeholder.empty'),
    createTimeText: normalizeText(record?.createTime$ || record?.createTime) || t('common.placeholder.empty'),
    extendFields:
      record?.extendFields && typeof record.extendFields === 'object' && !Array.isArray(record.extendFields)
        ? record.extendFields
        : {}
    ...dynamicValues,
    extendFields
  }
}
export function normalizeWhMatDetail(record = {}, t = $t) {
  return normalizeWhMatRow(record, t)
export function normalizeWhMatRow(record = {}, t = $t, enabledFields = []) {
  const statusValue = normalizeNullableNumber(record?.status)
  const validWarn = record?.validWarn ?? record?.valid_warn
  const purUnit = record?.purUnit ?? record?.purchaseUnit
  const stockLevelLabel =
    getWhMatStockLevelOptions().find(
      (item) => item.value === normalizeNullableNumber(record?.stockLevel)
    )?.label || ''
  const flagLabelManageLabel =
    getWhMatFlagLabelManageOptions(t).find(
      (item) => item.value === normalizeNullableNumber(record?.flagLabelMange)
    )?.label || ''
  return attachWhMatDynamicFields(
    {
      ...record,
      id: normalizeNullableNumber(record?.id),
      code: normalizeText(record?.code) || t('common.placeholder.empty'),
      name: normalizeText(record?.name) || t('common.placeholder.empty'),
      groupId: normalizeNullableNumber(record?.groupId),
      groupName:
        normalizeText(record?.groupId$ || record?.groupCode || record?.groupName) ||
        t('common.placeholder.empty'),
      shipperName:
        normalizeText(record?.shipperId$ || record?.shipperName) || t('common.placeholder.empty'),
      barcode: normalizeText(record?.barcode) || t('common.placeholder.empty'),
      platCode: normalizeText(record?.platCode) || t('common.placeholder.empty'),
      spec: normalizeText(record?.spec) || t('common.placeholder.empty'),
      model: normalizeText(record?.model) || t('common.placeholder.empty'),
      color: normalizeText(record?.color) || t('common.placeholder.empty'),
      size: normalizeText(record?.size) || t('common.placeholder.empty'),
      weight: record?.weight ?? t('common.placeholder.empty'),
      nromNum: record?.nromNum ?? t('common.placeholder.empty'),
      unit: normalizeText(record?.unit) || t('common.placeholder.empty'),
      purUnit: normalizeText(purUnit) || t('common.placeholder.empty'),
      stockUnit: normalizeText(record?.stockUnit) || t('common.placeholder.empty'),
      stockLevelText:
        normalizeText(record?.stockLeval$ || record?.stockLevel$ || stockLevelLabel) ||
        t('common.placeholder.empty'),
      flagLabelManageText:
        normalizeText(record?.flagLabelMange$ || record?.isLabelMange$ || flagLabelManageLabel) ||
        t('common.placeholder.empty'),
      flagCheckText: normalizeBooleanLikeText(record?.flagCheck, t),
      statusText:
        normalizeText(record?.status$) ||
        (statusValue === 1
          ? t('common.status.normal')
          : statusValue === 0
            ? t('common.status.frozen')
            : t('common.placeholder.empty')),
      statusType: statusValue === 1 ? 'success' : statusValue === 0 ? 'danger' : 'info',
      safeQty: record?.safeQty ?? t('common.placeholder.empty'),
      minQty: record?.minQty ?? t('common.placeholder.empty'),
      maxQty: record?.maxQty ?? t('common.placeholder.empty'),
      valid: record?.valid ?? t('common.placeholder.empty'),
      validWarn: validWarn ?? t('common.placeholder.empty'),
      stagn: record?.stagn ?? t('common.placeholder.empty'),
      describle: normalizeText(record?.describle) || t('common.placeholder.empty'),
      baseUnit: normalizeText(record?.baseUnit) || t('common.placeholder.empty'),
      useOrgName: normalizeText(record?.useOrgName) || t('common.placeholder.empty'),
      erpClsId: normalizeText(record?.erpClsId) || t('common.placeholder.empty'),
      rglarId: normalizeNullableNumber(record?.rglarId),
      rglarName:
        normalizeText(record?.rglarId$ || record?.rglarName || record?.rglarCode) ||
        t('common.placeholder.empty'),
      memo: normalizeText(record?.memo) || t('common.placeholder.empty'),
      updateByText: normalizeText(record?.updateBy$) || t('common.placeholder.empty'),
      createByText: normalizeText(record?.createBy$) || t('common.placeholder.empty'),
      updateTimeText:
        normalizeText(record?.updateTime$ || record?.updateTime) || t('common.placeholder.empty'),
      createTimeText:
        normalizeText(record?.createTime$ || record?.createTime) || t('common.placeholder.empty')
    },
    enabledFields
  )
}
export function normalizeWhMatDetail(record = {}, t = $t, enabledFields = []) {
  return normalizeWhMatRow(record, t, enabledFields)
}
export function buildWhMatDialogModel(record = {}) {
  const source = normalizeWhMatRow(record)
  return {
    ...createWhMatFormState(),
    id: source.id ?? void 0,
    code: normalizeText(record?.code),
    name: normalizeText(record?.name),
    groupId: source.groupId ?? '',
    useOrgName: normalizeText(record?.useOrgName),
    spec: normalizeText(record?.spec),
    model: normalizeText(record?.model),
    color: normalizeText(record?.color),
    size: normalizeText(record?.size),
    weight: normalizeNullableNumber(record?.weight),
    unit: normalizeText(record?.unit),
    purUnit: normalizeText(record?.purUnit || record?.purchaseUnit),
    describle: normalizeText(record?.describle),
    safeQty: normalizeNullableNumber(record?.safeQty),
    minQty: normalizeNullableNumber(record?.minQty),
    maxQty: normalizeNullableNumber(record?.maxQty),
    stagn: normalizeNullableInteger(record?.stagn),
    valid: normalizeNullableInteger(record?.valid),
    validWarn: normalizeNullableInteger(record?.validWarn),
    flagCheck: normalizeNullableInteger(record?.flagCheck) ?? 0,
    rglarId: normalizeNullableNumber(record?.rglarId) ?? ''
  }
}
export function buildWhMatSavePayload(formData = {}) {
  const payload = {
    ...(formData.id !== undefined && formData.id !== null ? { id: Number(formData.id) } : {}),
    code: normalizeText(formData.code),
    name: normalizeText(formData.name),
    groupId:
      formData.groupId !== undefined && formData.groupId !== null && formData.groupId !== ''
        ? Number(formData.groupId)
        : void 0,
    useOrgName: normalizeText(formData.useOrgName),
    spec: normalizeText(formData.spec),
    model: normalizeText(formData.model),
    color: normalizeText(formData.color),
    size: normalizeText(formData.size),
    weight: normalizeNullableNumber(formData.weight),
    unit: normalizeText(formData.unit),
    purUnit: normalizeText(formData.purUnit),
    describle: normalizeText(formData.describle),
    safeQty: normalizeNullableNumber(formData.safeQty),
    minQty: normalizeNullableNumber(formData.minQty),
    maxQty: normalizeNullableNumber(formData.maxQty),
    stagn: normalizeNullableInteger(formData.stagn),
    valid: normalizeNullableInteger(formData.valid),
    validWarn: normalizeNullableInteger(formData.validWarn),
    flagCheck: normalizeNullableInteger(formData.flagCheck),
    rglarId:
      formData.rglarId !== undefined && formData.rglarId !== null && formData.rglarId !== ''
        ? Number(formData.rglarId)
        : void 0
  }
  return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined))
}
export function getWhMatTreeNodeLabel(node = {}) {
@@ -150,6 +445,39 @@
  return [name, code].filter(Boolean).join(' · ') || $t('common.placeholder.empty')
}
export function buildWhMatPrintRows(records = [], t = $t) {
  return records.map((record) => {
    const normalizedRecord = normalizeWhMatRow(record, t)
    return {
      物料编码: normalizedRecord.code,
      物料名称: normalizedRecord.name,
      物料分组: normalizedRecord.groupName,
      规格: normalizedRecord.spec,
      型号: normalizedRecord.model,
      单位: normalizedRecord.unit,
      状态: normalizedRecord.statusText,
      更新时间: normalizedRecord.updateTimeText
    }
  })
}
export function buildWhMatReportMeta({
  previewMeta = {},
  count = 0,
  orientation = WH_MAT_REPORT_STYLE.orientation
} = {}) {
  return {
    reportTitle: WH_MAT_REPORT_TITLE,
    count,
    ...previewMeta,
    reportStyle: {
      ...WH_MAT_REPORT_STYLE,
      ...(previewMeta.reportStyle || {}),
      orientation
    }
  }
}
export const buildMatnrPageQueryParams = buildWhMatPageQueryParams
export const buildMatnrGroupTreeQueryParams = buildWhMatGroupTreeQueryParams
export const normalizeMatnrGroupTreeRows = normalizeWhMatGroupTreeRows
rsf-design/src/views/basic-info/wh-mat/whMatTable.columns.js
@@ -1,9 +1,67 @@
import { h } from 'vue'
import { ElTag } from 'element-plus'
import { $t } from '@/locales'
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
import { getWhMatDynamicFieldKey } from './whMatPage.helpers'
export function createWhMatTableColumns({ handleViewDetail, t = $t }) {
export function createWhMatTableColumns({
  handleViewDetail,
  handleEdit,
  handleDelete,
  handlePrint,
  enabledFields = [],
  canEdit = true,
  canDelete = true,
  t = $t
} = {}) {
  const operations = [{ key: 'view', label: t('common.actions.detail'), icon: 'ri:eye-line' }]
  if (canEdit && handleEdit) {
    operations.push({ key: 'edit', label: t('common.actions.edit'), icon: 'ri:pencil-line' })
  }
  if (handlePrint) {
    operations.push({ key: 'print', label: t('common.actions.print'), icon: 'ri:printer-line' })
  }
  if (canDelete && handleDelete) {
    operations.push({
      key: 'delete',
      label: t('common.actions.delete'),
      icon: 'ri:delete-bin-5-line',
      color: 'var(--art-error)'
    })
  }
  const dynamicColumns = Array.isArray(enabledFields)
    ? enabledFields.map((field) => ({
        prop: getWhMatDynamicFieldKey(field.fields),
        label: field.fieldsAlise,
        minWidth: 140,
        showOverflowTooltip: true,
        formatter: (row) => row[getWhMatDynamicFieldKey(field.fields)] || '--'
      }))
    : []
  return [
    {
      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: 'code',
      label: t('pages.basicInfo.whMat.table.code'),
@@ -19,7 +77,7 @@
    {
      prop: 'groupName',
      label: t('pages.basicInfo.whMat.table.groupName'),
      minWidth: 160,
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
@@ -31,15 +89,16 @@
    {
      prop: 'spec',
      label: t('pages.basicInfo.whMat.table.spec'),
      minWidth: 150,
      minWidth: 160,
      showOverflowTooltip: true
    },
    {
      prop: 'model',
      label: t('pages.basicInfo.whMat.table.model'),
      minWidth: 150,
      minWidth: 160,
      showOverflowTooltip: true
    },
    ...dynamicColumns,
    {
      prop: 'unit',
      label: t('table.unit'),
@@ -51,7 +110,17 @@
      width: 100,
      align: 'center',
      formatter: (row) =>
        h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || t('common.placeholder.empty'))
        h(
          ElTag,
          { type: row.statusType || 'info', effect: 'light' },
          () => row.statusText || t('common.placeholder.empty')
        )
    },
    {
      prop: 'updateByText',
      label: t('table.updateBy'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'updateTimeText',
@@ -60,12 +129,43 @@
      showOverflowTooltip: true
    },
    {
      prop: 'action',
      prop: 'createByText',
      label: t('table.createBy'),
      minWidth: 120,
      showOverflowTooltip: true
    },
    {
      prop: 'createTimeText',
      label: t('table.createTime'),
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
      prop: 'memo',
      label: t('table.memo'),
      minWidth: 160,
      showOverflowTooltip: true
    },
    {
      prop: 'operation',
      label: t('table.operation'),
      width: 100,
      fixed: 'right',
      width: 120,
      align: 'center',
      useSlot: true
      fixed: 'right',
      formatter: (row) =>
        h(
          'div',
          { class: 'flex justify-center' },
          h(ArtButtonMore, {
            list: operations,
            onClick: (item) => {
              if (item.key === 'view') handleViewDetail?.(row)
              if (item.key === 'edit') handleEdit?.(row)
              if (item.key === 'print') handlePrint?.(row)
              if (item.key === 'delete') handleDelete?.(row)
            }
          })
        )
    }
  ]
}