zhou zhou
2 天以前 24fa9ce6bc3f9c958958d42b1bb9a54a5372089f
feat: complete wh-mat page migration
6个文件已添加
2个文件已修改
849 ■■■■■ 已修改文件
rsf-design/src/api/wh-mat.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/adapters/backendMenuAdapter.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/routes/staticRoutes.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/index.vue 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-detail-drawer.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/whMatPage.helpers.js 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/basic-info/wh-mat/whMatTable.columns.js 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/wh-mat.js
New file
@@ -0,0 +1,54 @@
import request from '@/utils/http'
function normalizeText(value) {
  return String(value ?? '').trim()
}
function normalizeQueryParams(params = {}) {
  const result = {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20
  }
  ;['condition', 'code', 'name', 'spec', 'model', 'color', 'size', 'barcode', 'groupId'].forEach((key) => {
    const value = params[key]
    if (value === undefined || value === null || value === '') {
      return
    }
    if (typeof value === 'string') {
      const trimmed = normalizeText(value)
      if (trimmed) {
        result[key] = trimmed
      }
      return
    }
    result[key] = value
  })
  return result
}
function normalizeGroupTreeParams(params = {}) {
  return {
    condition: normalizeText(params.condition)
  }
}
export function fetchMatnrPage(params = {}) {
  return request.post({
    url: '/matnr/page',
    params: normalizeQueryParams(params)
  })
}
export function fetchMatnrDetail(id) {
  return request.get({ url: `/matnr/${id}` })
}
export function fetchMatnrGroupTree(params = {}) {
  return request.post({
    url: '/matnrGroup/tree',
    params: normalizeGroupTreeParams(params)
  })
}
rsf-design/src/router/adapters/backendMenuAdapter.js
@@ -5,7 +5,32 @@
  console: '/dashboard/console',
  user: '/system/user',
  role: '/system/role',
  aiParam: '/system/ai-param',
  aiPrompt: '/system/ai-prompt',
  aiCallLog: '/system/ai-observe',
  aiMcpMount: '/system/ai-mcp-mount',
  dept: '/system/dept',
  tenant: '/system/tenant',
  host: '/system/host',
  menu: '/system/menu',
  config: '/system/config',
  dictType: '/system/dict-type',
  fields: '/system/fields',
  fieldsItem: '/system/fields-item',
  whMat: '/basic-info/wh-mat',
  matnr: '/basic-info/wh-mat',
  warehouseStock: '/stock/warehouse-stock',
  warehouseAreasItem: '/stock/warehouse-areas-item',
  qlyInspect: '/manager/qly-inspect',
  locRevise: '/manager/loc-revise',
  freeze: '/manager/freeze',
  stock: '/manager/stock',
  task: '/manager/task',
  locPreview: '/manager/loc-preview',
  waveRule: '/manager/wave-rule',
  menuPda: '/manager/menu-pda',
  serialRule: '/system/serial-rule',
  operationRecord: '/system/operation-record',
  userLogin: '/system/user-login'
}
@@ -107,6 +132,10 @@
    return ''
  }
  if (hasChildren && normalizedKey && !PHASE_1_COMPONENTS[normalizedKey]) {
    return ''
  }
  if (!normalizedKey) {
    return normalizeComponentPath(fullRoutePath)
  }
rsf-design/src/router/routes/staticRoutes.js
@@ -7,6 +7,43 @@
  //   meta: { title: 'menus.dashboard.title' }
  // },
  {
    path: '/dashboard',
    component: () => import('@views/index/index.vue'),
    name: 'Dashboard',
    meta: { title: 'menus.dashboard.title' },
    children: [
      {
        path: 'console',
        name: 'Console',
        component: () => import('@views/dashboard/console/index.vue'),
        meta: {
          title: 'menus.dashboard.console',
          icon: 'ri:home-smile-2-line',
          keepAlive: false,
          fixedTab: true
        }
      }
    ]
  },
  {
    path: '/basic-info',
    component: () => import('@views/index/index.vue'),
    name: 'BasicInfo',
    meta: { title: 'menu.basicInfo' },
    children: [
      {
        path: 'wh-mat',
        name: 'WhMat',
        component: () => import('@views/basic-info/wh-mat/index.vue'),
        meta: {
          title: 'menu.matnr',
          icon: 'ri:bill-line',
          keepAlive: false
        }
      }
    ]
  },
  {
    path: '/auth/login',
    name: 'Login',
    component: () => import('@views/auth/login/index.vue'),
rsf-design/src/views/basic-info/wh-mat/index.vue
New file
@@ -0,0 +1,329 @@
<template>
  <div class="wh-mat-page art-full-height flex flex-col gap-4 xl:flex-row">
    <ElCard class="w-full shrink-0 xl:w-[320px]">
      <div class="mb-3 flex items-center justify-between gap-3">
        <div>
          <div class="text-base font-medium text-[var(--art-text-primary)]">物料分组</div>
          <div class="text-xs text-[var(--art-text-secondary)]">
            {{ selectedGroupLabel }}
          </div>
        </div>
        <ElButton text @click="handleResetGroup">全部</ElButton>
      </div>
      <div class="mb-3 flex items-center gap-2">
        <ElInput
          v-model.trim="groupSearch"
          clearable
          placeholder="搜索物料分组"
          @clear="handleGroupSearch"
          @keyup.enter="handleGroupSearch"
        />
        <ElButton @click="handleGroupSearch">搜索</ElButton>
      </div>
      <ElScrollbar class="h-[calc(100vh-260px)] pr-1">
        <div v-if="groupTreeLoading" class="py-6">
          <ElSkeleton :rows="10" animated />
        </div>
        <ElEmpty v-else-if="!groupTreeData.length" description="暂无物料分组" />
        <ElTree
          v-else
          :data="groupTreeData"
          :props="treeProps"
          node-key="id"
          highlight-current
          default-expand-all
          :current-node-key="selectedGroupId"
          @node-click="handleGroupNodeClick"
        >
          <template #default="{ data }">
            <div class="flex items-center gap-2">
              <span class="font-medium">{{ data.name || '--' }}</span>
              <span class="text-xs text-[var(--art-text-secondary)]">{{ data.code || '--' }}</span>
            </div>
          </template>
        </ElTree>
      </ElScrollbar>
    </ElCard>
    <div class="min-w-0 flex-1 space-y-4">
      <ArtSearchBar
        v-model="searchForm"
        :items="searchItems"
        :showExpand="true"
        @search="handleSearch"
        @reset="handleReset"
      />
      <ElCard class="art-table-card">
        <ArtTableHeader :loading="loading" v-model:columns="columnChecks" @refresh="loadMatnrList" />
      <ArtTable
        :loading="loading"
        :data="tableData"
        :columns="columns"
        :pagination="pagination"
        @pagination:size-change="handleSizeChange"
        @pagination:current-change="handleCurrentChange"
      >
        <template #action="{ row }">
          <ArtButtonTable icon="ri:eye-line" @click="openDetailDrawer(row)" />
        </template>
      </ArtTable>
    </ElCard>
    </div>
    <WhMatDetailDrawer v-model:visible="detailDrawerVisible" :loading="detailLoading" :detail="detailData" />
  </div>
</template>
<script setup>
  import { ElMessage } from 'element-plus'
  import { computed, onMounted, reactive, ref } from 'vue'
  import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
  import { useTableColumns } from '@/hooks/core/useTableColumns'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { fetchMatnrDetail, fetchMatnrGroupTree, fetchMatnrPage } from '@/api/wh-mat'
  import WhMatDetailDrawer from './modules/wh-mat-detail-drawer.vue'
  import { createWhMatTableColumns } from './whMatTable.columns'
  import {
    buildMatnrGroupTreeQueryParams,
    buildMatnrPageQueryParams,
    createWhMatSearchState,
    getWhMatTreeNodeLabel,
    normalizeMatnrDetail,
    normalizeMatnrGroupTreeRows,
    normalizeMatnrRow
  } from './whMatPage.helpers'
  defineOptions({ name: 'WhMat' })
  const loading = ref(false)
  const groupTreeLoading = ref(false)
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const tableData = ref([])
  const groupTreeData = ref([])
  const detailData = ref({})
  const selectedGroupId = ref(null)
  const groupSearch = ref('')
  const searchForm = ref(createWhMatSearchState())
  const pagination = reactive({
    current: 1,
    size: 20,
    total: 0
  })
  const treeProps = {
    label: 'name',
    children: 'children'
  }
  const searchItems = computed(() => [
    {
      label: '关键字',
      key: 'condition',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入物料编码/物料名称'
      }
    },
    {
      label: '物料编码',
      key: 'code',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入物料编码'
      }
    },
    {
      label: '物料名称',
      key: 'name',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入物料名称'
      }
    },
    {
      label: '规格',
      key: 'spec',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入规格'
      }
    },
    {
      label: '条码',
      key: 'barcode',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入条码'
      }
    }
  ])
  const { columnChecks, columns } = useTableColumns(() =>
    createWhMatTableColumns({
      handleViewDetail: openDetailDrawer
    })
  )
  const selectedGroupLabel = computed(() => {
    if (!selectedGroupId.value) {
      return '全部物料'
    }
    const found = findGroupNode(groupTreeData.value, selectedGroupId.value)
    return found ? getWhMatTreeNodeLabel(found) : '全部物料'
  })
  function findGroupNode(nodes, targetId) {
    const normalizedTarget = String(targetId || '')
    for (const node of nodes || []) {
      if (String(node.id) === normalizedTarget) {
        return node
      }
      if (node.children?.length) {
        const child = findGroupNode(node.children, normalizedTarget)
        if (child) {
          return child
        }
      }
    }
    return null
  }
  function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
    target.total = Number(response?.total || 0)
    target.current = Number(response?.current || fallbackCurrent || 1)
    target.size = Number(response?.size || fallbackSize || target.size || 20)
  }
  async function loadGroupTree() {
    groupTreeLoading.value = true
    try {
      const records = await guardRequestWithMessage(
        fetchMatnrGroupTree(buildMatnrGroupTreeQueryParams({ condition: groupSearch.value })),
        [],
        { timeoutMessage: '物料分组加载超时,已停止等待' }
      )
      const normalizedTree = normalizeMatnrGroupTreeRows(Array.isArray(records) ? records : [])
      groupTreeData.value = normalizedTree
      if (selectedGroupId.value && !findGroupNode(normalizedTree, selectedGroupId.value)) {
        selectedGroupId.value = null
      }
    } catch (error) {
      groupTreeData.value = []
      ElMessage.error(error?.message || '物料分组加载失败')
    } finally {
      groupTreeLoading.value = false
    }
  }
  async function loadMatnrList() {
    loading.value = true
    try {
      const response = await guardRequestWithMessage(
        fetchMatnrPage(
          buildMatnrPageQueryParams({
            ...searchForm.value,
            groupId: selectedGroupId.value,
            current: pagination.current,
            pageSize: pagination.size
          })
        ),
        {
          records: [],
          total: 0,
          current: pagination.current,
          size: pagination.size
        },
        { timeoutMessage: '物料列表加载超时,已停止等待' }
      )
      tableData.value = Array.isArray(response?.records)
        ? response.records.map((record) => normalizeMatnrRow(record))
        : []
      updatePaginationState(pagination, response, pagination.current, pagination.size)
    } catch (error) {
      tableData.value = []
      ElMessage.error(error?.message || '物料列表加载失败')
    } finally {
      loading.value = false
    }
  }
  async function openDetailDrawer(row) {
    detailDrawerVisible.value = true
    detailLoading.value = true
    try {
      detailData.value = normalizeMatnrDetail(
        await guardRequestWithMessage(fetchMatnrDetail(row.id), {}, {
          timeoutMessage: '物料详情加载超时,已停止等待'
        })
      )
    } catch (error) {
      detailDrawerVisible.value = false
      detailData.value = {}
      ElMessage.error(error?.message || '获取物料详情失败')
    } finally {
      detailLoading.value = false
    }
  }
  function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params
    }
    pagination.current = 1
    loadMatnrList()
  }
  async function handleReset() {
    searchForm.value = createWhMatSearchState()
    pagination.current = 1
    selectedGroupId.value = null
    groupSearch.value = ''
    await Promise.all([loadGroupTree(), loadMatnrList()])
  }
  function handleSizeChange(size) {
    pagination.size = size
    pagination.current = 1
    loadMatnrList()
  }
  function handleCurrentChange(current) {
    pagination.current = current
    loadMatnrList()
  }
  function handleGroupNodeClick(data) {
    selectedGroupId.value = data?.id ?? null
    pagination.current = 1
    loadMatnrList()
  }
  async function handleResetGroup() {
    selectedGroupId.value = null
    pagination.current = 1
    groupSearch.value = ''
    await Promise.all([loadGroupTree(), loadMatnrList()])
  }
  async function handleGroupSearch() {
    selectedGroupId.value = null
    pagination.current = 1
    await Promise.all([loadGroupTree(), loadMatnrList()])
  }
  onMounted(async () => {
    await Promise.all([loadGroupTree(), loadMatnrList()])
  })
</script>
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-detail-drawer.vue
New file
@@ -0,0 +1,100 @@
<template>
  <ElDrawer
    :model-value="visible"
    title="物料详情"
    size="960px"
    destroy-on-close
    @update:model-value="handleVisibleChange"
  >
    <ElScrollbar class="h-[calc(100vh-180px)] pr-1">
      <div v-if="loading" class="py-6">
        <ElSkeleton :rows="12" animated />
      </div>
      <div v-else class="space-y-4">
        <ElDescriptions title="基础信息" :column="2" border>
          <ElDescriptionsItem label="物料编码">{{ detail.code || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="物料名称">{{ detail.name || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="物料分组">{{ detail.groupName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="货主">{{ detail.shipperName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="条码">{{ detail.barcode || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="规格">{{ detail.spec || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="型号">{{ detail.model || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="颜色">{{ detail.color || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="尺寸">{{ detail.size || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="描述">{{ detail.describle || '--' }}</ElDescriptionsItem>
        </ElDescriptions>
        <ElDescriptions title="库存属性" :column="2" border>
          <ElDescriptionsItem label="单位">{{ detail.unit || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="采购单位">{{ detail.purUnit || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="库位单位">{{ detail.stockUnit || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="出入库优先级">{{ detail.stockLevelText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="是否标签管理">{{ detail.flagLabelManageText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="是否免检">{{ detail.flagCheckText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="安全库存">{{ detail.safeQty ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="最小库存预警">{{ detail.minQty ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="最大库存预警">{{ detail.maxQty ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="停滞天数">{{ detail.stagn ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="保质期天数">{{ detail.valid ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="效期预警阈值">{{ detail.validWarn ?? '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="状态">{{ detail.statusText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="基础单位">{{ detail.baseUnit || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="使用组织">{{ detail.useOrgName || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="ERP分类">{{ detail.erpClsId || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="备注">{{ detail.memo || '--' }}</ElDescriptionsItem>
        </ElDescriptions>
        <ElDescriptions title="审计信息" :column="2" border>
          <ElDescriptionsItem label="创建人">{{ detail.createByText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="创建时间">{{ detail.createTimeText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="更新人">{{ detail.updateByText || '--' }}</ElDescriptionsItem>
          <ElDescriptionsItem label="更新时间">{{ detail.updateTimeText || '--' }}</ElDescriptionsItem>
        </ElDescriptions>
        <ElDescriptions v-if="extendFieldEntries.length" title="扩展字段" :column="2" border>
          <ElDescriptionsItem
            v-for="entry in extendFieldEntries"
            :key="entry.key"
            :label="entry.key"
          >
            {{ entry.value || '--' }}
          </ElDescriptionsItem>
        </ElDescriptions>
      </div>
    </ElScrollbar>
  </ElDrawer>
</template>
<script setup>
  import { computed } from 'vue'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    loading: { type: Boolean, default: false },
    detail: { type: Object, default: () => ({}) }
  })
  const emit = defineEmits(['update:visible'])
  const visible = computed({
    get: () => props.visible,
    set: (value) => emit('update:visible', value)
  })
  const extendFieldEntries = computed(() => {
    const fields = props.detail?.extendFields
    if (!fields || typeof fields !== 'object') {
      return []
    }
    return Object.entries(fields)
      .map(([key, value]) => ({
        key,
        value: String(value ?? '').trim()
      }))
      .filter((item) => item.key)
  })
  function handleVisibleChange(value) {
    visible.value = value
  }
</script>
rsf-design/src/views/basic-info/wh-mat/whMatPage.helpers.js
New file
@@ -0,0 +1,151 @@
function normalizeText(value) {
  return String(value ?? '').trim()
}
function normalizeNumber(value, fallback = 0) {
  if (value === '' || value === null || value === undefined) {
    return fallback
  }
  const numericValue = Number(value)
  return Number.isFinite(numericValue) ? numericValue : fallback
}
function normalizeNullableNumber(value) {
  if (value === '' || value === null || value === undefined) {
    return null
  }
  const numericValue = Number(value)
  return Number.isFinite(numericValue) ? numericValue : null
}
export function createWhMatSearchState() {
  return {
    condition: '',
    code: '',
    name: '',
    spec: '',
    model: '',
    barcode: ''
  }
}
export function buildWhMatPageQueryParams(params = {}) {
  const result = {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20
  }
  ;['condition', 'code', 'name', 'spec', 'model', 'barcode'].forEach((key) => {
    const value = normalizeText(params[key])
    if (value) {
      result[key] = value
    }
  })
  if (params.groupId !== undefined && params.groupId !== null && params.groupId !== '') {
    result.groupId = String(params.groupId)
  }
  return result
}
export function buildWhMatGroupTreeQueryParams(params = {}) {
  return {
    condition: normalizeText(params.condition)
  }
}
export function normalizeWhMatGroupTreeRows(records = []) {
  if (!Array.isArray(records)) {
    return []
  }
  return records.map((item) => {
    const children = normalizeWhMatGroupTreeRows(item?.children || [])
    const id = normalizeNullableNumber(item?.id)
    const code = normalizeText(item?.code)
    const name = normalizeText(item?.name)
    const label = [name, code].filter(Boolean).join(' · ') || '-'
    return {
      ...item,
      id,
      parentId: normalizeNumber(item?.parentId, 0),
      code,
      name,
      label,
      displayLabel: label,
      status: normalizeNullableNumber(item?.status),
      statusText: normalizeNumber(item?.status, 1) === 1 ? '正常' : '冻结',
      statusType: normalizeNumber(item?.status, 1) === 1 ? 'success' : 'danger',
      memo: normalizeText(item?.memo) || '-',
      children
    }
  })
}
export function normalizeWhMatRow(record = {}) {
  const statusValue = normalizeNullableNumber(record?.status)
  return {
    ...record,
    code: normalizeText(record?.code) || '-',
    name: normalizeText(record?.name) || '-',
    groupName: normalizeText(record?.groupId$ || record?.groupCode) || '-',
    shipperName: normalizeText(record?.shipperId$ || record?.shipperName) || '-',
    barcode: normalizeText(record?.barcode) || '-',
    spec: normalizeText(record?.spec) || '-',
    model: normalizeText(record?.model) || '-',
    color: normalizeText(record?.color) || '-',
    size: normalizeText(record?.size) || '-',
    unit: normalizeText(record?.unit) || '-',
    purUnit: normalizeText(record?.purUnit) || '-',
    stockUnit: normalizeText(record?.stockUnit) || '-',
    stockLevelText: normalizeText(record?.stockLeval$) || '-',
    flagLabelManageText: normalizeText(record?.flagLabelMange$) || '-',
    flagCheckText:
      record?.flagCheck === 1 || record?.flagCheck === '1'
        ? '是'
        : record?.flagCheck === 0 || record?.flagCheck === '0'
          ? '否'
          : '-',
    statusText: normalizeText(record?.status$) || (statusValue === 1 ? '正常' : statusValue === 0 ? '冻结' : '-'),
    statusType: statusValue === 1 ? 'success' : statusValue === 0 ? 'danger' : 'info',
    safeQty: record?.safeQty ?? '-',
    minQty: record?.minQty ?? '-',
    maxQty: record?.maxQty ?? '-',
    valid: record?.valid ?? '-',
    validWarn: record?.validWarn ?? '-',
    stagn: record?.stagn ?? '-',
    describle: normalizeText(record?.describle) || '-',
    baseUnit: normalizeText(record?.baseUnit) || '-',
    useOrgName: normalizeText(record?.useOrgName) || '-',
    erpClsId: normalizeText(record?.erpClsId) || '-',
    memo: normalizeText(record?.memo) || '-',
    updateByText: normalizeText(record?.updateBy$) || '-',
    createByText: normalizeText(record?.createBy$) || '-',
    updateTimeText: normalizeText(record?.updateTime$ || record?.updateTime) || '-',
    createTimeText: normalizeText(record?.createTime$ || record?.createTime) || '-',
    extendFields:
      record?.extendFields && typeof record.extendFields === 'object' && !Array.isArray(record.extendFields)
        ? record.extendFields
        : {}
  }
}
export function normalizeWhMatDetail(record = {}) {
  return normalizeWhMatRow(record)
}
export function getWhMatTreeNodeLabel(node = {}) {
  const name = normalizeText(node?.name)
  const code = normalizeText(node?.code)
  return [name, code].filter(Boolean).join(' · ') || '-'
}
export const buildMatnrPageQueryParams = buildWhMatPageQueryParams
export const buildMatnrGroupTreeQueryParams = buildWhMatGroupTreeQueryParams
export const normalizeMatnrGroupTreeRows = normalizeWhMatGroupTreeRows
export const normalizeMatnrRow = normalizeWhMatRow
export const normalizeMatnrDetail = normalizeWhMatDetail
export const createMatnrSearchState = createWhMatSearchState
export const getMatnrTreeNodeLabel = getWhMatTreeNodeLabel
rsf-design/src/views/basic-info/wh-mat/whMatTable.columns.js
New file
@@ -0,0 +1,70 @@
import { h } from 'vue'
import { ElTag } from 'element-plus'
export function createWhMatTableColumns({ handleViewDetail }) {
  return [
    {
      prop: 'code',
      label: '物料编码',
      minWidth: 150,
      showOverflowTooltip: true
    },
    {
      prop: 'name',
      label: '物料名称',
      minWidth: 220,
      showOverflowTooltip: true
    },
    {
      prop: 'groupName',
      label: '物料分组',
      minWidth: 160,
      showOverflowTooltip: true
    },
    {
      prop: 'barcode',
      label: '条码',
      minWidth: 160,
      showOverflowTooltip: true
    },
    {
      prop: 'spec',
      label: '规格',
      minWidth: 150,
      showOverflowTooltip: true
    },
    {
      prop: 'model',
      label: '型号',
      minWidth: 150,
      showOverflowTooltip: true
    },
    {
      prop: 'unit',
      label: '单位',
      width: 100
    },
    {
      prop: 'status',
      label: '状态',
      width: 100,
      align: 'center',
      formatter: (row) =>
        h(ElTag, { type: row.statusType || 'info', effect: 'light' }, () => row.statusText || '-')
    },
    {
      prop: 'updateTimeText',
      label: '更新时间',
      minWidth: 180,
      showOverflowTooltip: true
    },
    {
      prop: 'action',
      label: '操作',
      width: 100,
      fixed: 'right',
      align: 'center',
      useSlot: true
    }
  ]
}
rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs
New file
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict'
import test from 'node:test'
test('builds matnr page params with trimmed filters and group ids', async () => {
  const { buildMatnrPageQueryParams } = await import(
    '../src/views/basic-info/wh-mat/whMatPage.helpers.js'
  )
  assert.deepEqual(
    buildMatnrPageQueryParams({
      current: 2,
      pageSize: 30,
      condition: '  半成品  ',
      code: '  RM001  ',
      name: '  物料A  ',
      spec: '',
      groupId: 19
    }),
    {
      current: 2,
      pageSize: 30,
      condition: '半成品',
      code: 'RM001',
      name: '物料A',
      groupId: '19'
    }
  )
})
test('normalizes matnr group trees for el-tree rendering', async () => {
  const { normalizeMatnrGroupTreeRows } = await import(
    '../src/views/basic-info/wh-mat/whMatPage.helpers.js'
  )
  const tree = normalizeMatnrGroupTreeRows([
    {
      id: 1,
      parentId: 0,
      name: '半成品',
      code: 'RM',
      status: 1,
      children: [
        {
          id: 2,
          parentId: 1,
          name: '半成品A',
          code: 'RM-A',
          status: 0
        }
      ]
    }
  ])
  assert.equal(tree[0].displayLabel, '半成品 · RM')
  assert.equal(tree[0].children[0].statusText, '冻结')
})
test('normalizes matnr detail fields for detail drawer display', async () => {
  const { normalizeMatnrDetail } = await import('../src/views/basic-info/wh-mat/whMatPage.helpers.js')
  const detail = normalizeMatnrDetail({
    id: 8,
    code: 'RM001',
    name: '半成品',
    groupId$: '原材料',
    status: 1,
    stockLeval$: ' A',
    flagLabelMange$: ' 是',
    extendFields: {
      batch: 'B001'
    }
  })
  assert.equal(detail.code, 'RM001')
  assert.equal(detail.groupName, '原材料')
  assert.equal(detail.statusText, '正常')
  assert.equal(detail.extendFields.batch, 'B001')
})