From 24fa9ce6bc3f9c958958d42b1bb9a54a5372089f Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 16:35:15 +0800
Subject: [PATCH] feat: complete wh-mat page migration
---
rsf-design/src/router/adapters/backendMenuAdapter.js | 29 +
rsf-design/src/api/wh-mat.js | 54 +++
rsf-design/src/router/routes/staticRoutes.js | 37 ++
rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs | 79 +++++
rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-detail-drawer.vue | 100 ++++++
rsf-design/src/views/basic-info/wh-mat/whMatPage.helpers.js | 151 ++++++++++
rsf-design/src/views/basic-info/wh-mat/index.vue | 329 +++++++++++++++++++++
rsf-design/src/views/basic-info/wh-mat/whMatTable.columns.js | 70 ++++
8 files changed, 849 insertions(+), 0 deletions(-)
diff --git a/rsf-design/src/api/wh-mat.js b/rsf-design/src/api/wh-mat.js
new file mode 100644
index 0000000..ed04047
--- /dev/null
+++ b/rsf-design/src/api/wh-mat.js
@@ -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)
+ })
+}
+
diff --git a/rsf-design/src/router/adapters/backendMenuAdapter.js b/rsf-design/src/router/adapters/backendMenuAdapter.js
index 41219c9..8aa8538 100644
--- a/rsf-design/src/router/adapters/backendMenuAdapter.js
+++ b/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)
}
diff --git a/rsf-design/src/router/routes/staticRoutes.js b/rsf-design/src/router/routes/staticRoutes.js
index 9dc9903..7057f97 100644
--- a/rsf-design/src/router/routes/staticRoutes.js
+++ b/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'),
diff --git a/rsf-design/src/views/basic-info/wh-mat/index.vue b/rsf-design/src/views/basic-info/wh-mat/index.vue
new file mode 100644
index 0000000..72d23c5
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/index.vue
@@ -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>
diff --git a/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-detail-drawer.vue b/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-detail-drawer.vue
new file mode 100644
index 0000000..a5cb07e
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/modules/wh-mat-detail-drawer.vue
@@ -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>
diff --git a/rsf-design/src/views/basic-info/wh-mat/whMatPage.helpers.js b/rsf-design/src/views/basic-info/wh-mat/whMatPage.helpers.js
new file mode 100644
index 0000000..6675a07
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/whMatPage.helpers.js
@@ -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 ? '姝e父' : '鍐荤粨',
+ 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 ? '姝e父' : 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
diff --git a/rsf-design/src/views/basic-info/wh-mat/whMatTable.columns.js b/rsf-design/src/views/basic-info/wh-mat/whMatTable.columns.js
new file mode 100644
index 0000000..3666937
--- /dev/null
+++ b/rsf-design/src/views/basic-info/wh-mat/whMatTable.columns.js
@@ -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
+ }
+ ]
+}
diff --git a/rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs b/rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs
new file mode 100644
index 0000000..77ce88b
--- /dev/null
+++ b/rsf-design/tests/basic-info-wh-mat-page-contract.test.mjs
@@ -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: '鍗婃垚鍝丄',
+ code: 'RM-A',
+ status: 0
+ }
+ ]
+ }
+ ])
+
+ assert.equal(tree[0].displayLabel, '鍗婃垚鍝� 路 RM')
+ assert.equal(tree[0].children[0].statusText, '鍐荤粨')
+})
+
+test('normalizes matnr detail fields for detail drawer display', async () => {
+ const { normalizeMatnrDetail } = await import('../src/views/basic-info/wh-mat/whMatPage.helpers.js')
+
+ const detail = normalizeMatnrDetail({
+ id: 8,
+ code: 'RM001',
+ name: '鍗婃垚鍝�',
+ groupId$: '鍘熸潗鏂�',
+ status: 1,
+ stockLeval$: ' A',
+ flagLabelMange$: ' 鏄�',
+ extendFields: {
+ batch: 'B001'
+ }
+ })
+
+ assert.equal(detail.code, 'RM001')
+ assert.equal(detail.groupName, '鍘熸潗鏂�')
+ assert.equal(detail.statusText, '姝e父')
+ assert.equal(detail.extendFields.batch, 'B001')
+})
+
--
Gitblit v1.9.1