From 1d95b134d85c3c60cf0e72739888c9741a0bb1ee Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期五, 10 四月 2026 13:20:39 +0800
Subject: [PATCH] #页面优化

---
 rsf-design/src/views/basic-info/wh-mat/index.vue |  996 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 961 insertions(+), 35 deletions(-)

diff --git a/rsf-design/src/views/basic-info/wh-mat/index.vue b/rsf-design/src/views/basic-info/wh-mat/index.vue
index fb852a1..6d28ea1 100644
--- a/rsf-design/src/views/basic-info/wh-mat/index.vue
+++ b/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 {

--
Gitblit v1.9.1