From adb016e4492d927ed3eb9fc098294ffc81c06ae3 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 13 四月 2026 14:20:06 +0800
Subject: [PATCH] #页面优化

---
 rsf-design/src/views/manager/loc-preview/index.vue                                      |  330 -----------------
 rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js                |   27 +
 rsf-design/src/views/stock/warehouse-stock/index.vue                                    |  221 +++++++++++
 rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue |   20 
 rsf-design/src/views/manager/loc-preview/modules/loc-preview-item-table.columns.js      |  161 ++++++++
 rsf-design/src/views/manager/loc-preview/modules/loc-preview-items-page.vue             |  236 ++++++++++++
 rsf-design/src/api/warehouse-stock.js                                                   |   18 
 rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js               |   81 ++++
 8 files changed, 748 insertions(+), 346 deletions(-)

diff --git a/rsf-design/src/api/warehouse-stock.js b/rsf-design/src/api/warehouse-stock.js
index edd01bd..d928af0 100644
--- a/rsf-design/src/api/warehouse-stock.js
+++ b/rsf-design/src/api/warehouse-stock.js
@@ -1,7 +1,8 @@
 import request from '@/utils/http'
 
 export function buildWarehouseStockPageParams(params = {}) {
-  const matnrCode = typeof params.matnrCode === 'string' ? params.matnrCode.trim() : params.matnrCode
+  const matnrCode =
+    typeof params.matnrCode === 'string' ? params.matnrCode.trim() : params.matnrCode
   const maktx = typeof params.maktx === 'string' ? params.maktx.trim() : params.maktx
   const batch = typeof params.batch === 'string' ? params.batch.trim() : params.batch
   return {
@@ -14,7 +15,9 @@
     ...Object.fromEntries(
       Object.entries(params).filter(
         ([key, value]) =>
-          !['current', 'pageSize', 'size', 'aggType', 'matnrCode', 'maktx', 'batch'].includes(key) &&
+          !['current', 'pageSize', 'size', 'aggType', 'matnrCode', 'maktx', 'batch'].includes(
+            key
+          ) &&
           value !== undefined &&
           value !== ''
       )
@@ -36,16 +39,23 @@
     current: params.current || 1,
     pageSize: params.pageSize || params.size || 20,
     ...(params.aggType !== undefined ? { aggType: params.aggType } : {}),
+    ...(params.orderBy !== undefined ? { orderBy: params.orderBy } : {}),
     ...(params.stock !== undefined ? { stock: params.stock } : {})
   }
 }
 
 export function fetchWarehouseStockPage(params = {}) {
-  return request.post({ url: '/warehouse/stock/page', params: buildWarehouseStockPageParams(params) })
+  return request.post({
+    url: '/warehouse/stock/page',
+    params: buildWarehouseStockPageParams(params)
+  })
 }
 
 export function fetchWarehouseStockInfoPage(params = {}) {
-  return request.post({ url: '/warehouse/stock/info', params: buildWarehouseStockInfoParams(params) })
+  return request.post({
+    url: '/warehouse/stock/info',
+    params: buildWarehouseStockInfoParams(params)
+  })
 }
 
 export function fetchWarehouseStockHistoriesPage(params = {}) {
diff --git a/rsf-design/src/views/manager/loc-preview/index.vue b/rsf-design/src/views/manager/loc-preview/index.vue
index fa48a2c..6d70ffb 100644
--- a/rsf-design/src/views/manager/loc-preview/index.vue
+++ b/rsf-design/src/views/manager/loc-preview/index.vue
@@ -1,335 +1,9 @@
 <template>
-  <div class="loc-preview-page art-full-height">
-    <ArtSearchBar
-      v-model="searchForm"
-      :items="searchItems"
-      :showExpand="false"
-      @search="handleSearch"
-      @reset="handleReset"
-    />
-
-    <ElCard class="art-table-card">
-      <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="loadPageData" />
-
-      <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>
-
-    <LocPreviewDetailDrawer
-      v-model:visible="detailDrawerVisible"
-      :loading="detailLoading"
-      :detail="activeLocDetail"
-      :data="detailTableData"
-      :columns="detailColumns"
-      :pagination="detailPagination"
-      @refresh="loadDetailResources"
-      @size-change="handleDetailSizeChange"
-      @current-change="handleDetailCurrentChange"
-    />
-  </div>
+  <LocPreviewItemsPage />
 </template>
 
 <script setup>
-  import { computed, onMounted, reactive, ref } from 'vue'
-  import { useI18n } from 'vue-i18n'
-  import { useTableColumns } from '@/hooks/core/useTableColumns'
-  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
-  import {
-    fetchEnabledFields,
-    fetchLocPreviewDetail,
-    fetchLocPreviewItemsPage,
-    fetchLocPreviewPage
-  } from '@/api/loc-preview'
-  import LocPreviewDetailDrawer from './modules/loc-preview-detail-drawer.vue'
-  import { createLocPreviewTableColumns } from './locPreviewTable.columns'
-  import {
-    buildLocPreviewPageQueryParams,
-    createLocPreviewSearchState,
-    getLocPreviewDynamicFieldKey,
-    normalizeLocPreviewDetail,
-    normalizeLocPreviewEnabledFields,
-    normalizeLocPreviewItemRow,
-    normalizeLocPreviewRow
-  } from './locPreviewPage.helpers'
+  import LocPreviewItemsPage from './modules/loc-preview-items-page.vue'
 
   defineOptions({ name: 'LocPreview' })
-  const { t } = useI18n()
-
-  const loading = ref(false)
-  const detailLoading = ref(false)
-  const tableData = ref([])
-  const detailTableData = ref([])
-  const detailDrawerVisible = ref(false)
-  const activeLocRow = ref(null)
-  const activeLocDetail = ref({})
-  const enabledFields = ref([])
-  const searchForm = ref(createLocPreviewSearchState())
-
-  const pagination = reactive({
-    current: 1,
-    size: 20,
-    total: 0
-  })
-
-  const detailPagination = reactive({
-    current: 1,
-    size: 20,
-    total: 0
-  })
-
-  const searchItems = computed(() => [
-    {
-      label: t('pages.manager.locPreview.search.condition'),
-      key: 'condition',
-      type: 'input',
-      props: {
-        clearable: true,
-        placeholder: t('pages.manager.locPreview.search.conditionPlaceholder')
-      }
-    },
-    {
-      label: t('pages.manager.locPreview.search.code'),
-      key: 'code',
-      type: 'input',
-      props: {
-        clearable: true,
-        placeholder: t('pages.manager.locPreview.search.codePlaceholder')
-      }
-    },
-    {
-      label: t('pages.manager.locPreview.search.barcode'),
-      key: 'barcode',
-      type: 'input',
-      props: {
-        clearable: true,
-        placeholder: t('pages.manager.locPreview.search.barcodePlaceholder')
-      }
-    }
-  ])
-
-  function createDetailColumns() {
-    return [
-      {
-        prop: 'locCode',
-        label: t('pages.manager.locPreview.table.locCode'),
-        minWidth: 140,
-        showOverflowTooltip: true
-      },
-      {
-        prop: 'wareArea',
-        label: t('pages.manager.locPreview.table.areaLabel'),
-        minWidth: 140,
-        showOverflowTooltip: true
-      },
-      {
-        prop: 'orderCode',
-        label: t('pages.orders.common.orderCode'),
-        minWidth: 180,
-        showOverflowTooltip: true
-      },
-      {
-        prop: 'matnrCode',
-        label: t('table.materialCode'),
-        minWidth: 160,
-        showOverflowTooltip: true
-      },
-      {
-        prop: 'maktx',
-        label: t('table.materialName'),
-        minWidth: 220,
-        showOverflowTooltip: true
-      },
-      {
-        prop: 'batch',
-        label: t('table.batch'),
-        minWidth: 140,
-        showOverflowTooltip: true
-      },
-      {
-        prop: 'trackCode',
-        label: t('pages.orders.common.trackCode'),
-        minWidth: 150,
-        showOverflowTooltip: true
-      },
-      {
-        prop: 'unit',
-        label: t('table.unit'),
-        width: 100
-      },
-      {
-        prop: 'anfme',
-        label: t('pages.manager.freeze.table.anfme'),
-        width: 120
-      },
-      {
-        prop: 'qty',
-        label: t('pages.manager.freeze.table.qty'),
-        width: 120
-      },
-      {
-        prop: 'workQty',
-        label: t('pages.manager.freeze.table.workQty'),
-        width: 120
-      },
-      ...enabledFields.value.map((field) => ({
-        prop: getLocPreviewDynamicFieldKey(field.fields),
-        label: field.fieldsAlise,
-        minWidth: 140,
-        showOverflowTooltip: true,
-        formatter: (row) => row[getLocPreviewDynamicFieldKey(field.fields)] || '-'
-      })),
-      {
-        prop: 'createTimeText',
-        label: t('table.createTime'),
-        minWidth: 180,
-        showOverflowTooltip: true
-      }
-    ]
-  }
-
-  const detailColumns = computed(() => createDetailColumns())
-
-  const { columns, columnChecks } = useTableColumns(() =>
-    createLocPreviewTableColumns({
-      handleViewDetail: openDetailDrawer
-    })
-  )
-
-  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 loadEnabledFieldDefinitions() {
-    const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
-      timeoutMessage: t('pages.manager.locPreview.messages.fieldsTimeout')
-    })
-    enabledFields.value = normalizeLocPreviewEnabledFields(fields)
-  }
-
-  async function loadPageData() {
-    loading.value = true
-    try {
-      const response = await guardRequestWithMessage(
-        fetchLocPreviewPage(
-          buildLocPreviewPageQueryParams({
-            ...searchForm.value,
-            current: pagination.current,
-            pageSize: pagination.size
-          })
-        ),
-        {
-          records: [],
-          total: 0,
-          current: pagination.current,
-          size: pagination.size
-        },
-        { timeoutMessage: t('pages.manager.locPreview.messages.pageTimeout') }
-      )
-      tableData.value = Array.isArray(response?.records)
-        ? response.records.map((record) => normalizeLocPreviewRow(record))
-        : []
-      updatePaginationState(pagination, response, pagination.current, pagination.size)
-    } finally {
-      loading.value = false
-    }
-  }
-
-  async function loadDetailResources() {
-    if (!activeLocRow.value?.id) {
-      return
-    }
-
-    detailLoading.value = true
-    try {
-      const [detailResponse, itemResponse] = await Promise.all([
-        guardRequestWithMessage(fetchLocPreviewDetail(activeLocRow.value.id), {}, {
-          timeoutMessage: t('pages.manager.locPreview.messages.detailTimeout')
-        }),
-        guardRequestWithMessage(
-          fetchLocPreviewItemsPage({
-            current: detailPagination.current,
-            pageSize: detailPagination.size,
-            locId: activeLocRow.value.id
-          }),
-          {
-            records: [],
-            total: 0,
-            current: detailPagination.current,
-            size: detailPagination.size
-          },
-          { timeoutMessage: t('pages.manager.locPreview.messages.itemPageTimeout') }
-        )
-      ])
-
-      activeLocDetail.value = normalizeLocPreviewDetail(detailResponse)
-      detailTableData.value = Array.isArray(itemResponse?.records)
-        ? itemResponse.records.map((record) => normalizeLocPreviewItemRow(record, enabledFields.value))
-        : []
-      updatePaginationState(detailPagination, itemResponse, detailPagination.current, detailPagination.size)
-    } finally {
-      detailLoading.value = false
-    }
-  }
-
-  function openDetailDrawer(row) {
-    activeLocRow.value = row
-    detailPagination.current = 1
-    detailDrawerVisible.value = true
-    loadDetailResources()
-  }
-
-  function handleSearch(params) {
-    searchForm.value = {
-      ...searchForm.value,
-      ...params
-    }
-    pagination.current = 1
-    loadPageData()
-  }
-
-  function handleReset() {
-    searchForm.value = createLocPreviewSearchState()
-    pagination.current = 1
-    pagination.size = 20
-    loadPageData()
-  }
-
-  function handleSizeChange(size) {
-    pagination.size = size
-    pagination.current = 1
-    loadPageData()
-  }
-
-  function handleCurrentChange(current) {
-    pagination.current = current
-    loadPageData()
-  }
-
-  function handleDetailSizeChange(size) {
-    detailPagination.size = size
-    detailPagination.current = 1
-    loadDetailResources()
-  }
-
-  function handleDetailCurrentChange(current) {
-    detailPagination.current = current
-    loadDetailResources()
-  }
-
-  onMounted(async () => {
-    await loadEnabledFieldDefinitions()
-    await loadPageData()
-  })
 </script>
diff --git a/rsf-design/src/views/manager/loc-preview/modules/loc-preview-item-table.columns.js b/rsf-design/src/views/manager/loc-preview/modules/loc-preview-item-table.columns.js
new file mode 100644
index 0000000..8aaca01
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-preview/modules/loc-preview-item-table.columns.js
@@ -0,0 +1,161 @@
+import { $t } from '@/locales'
+
+export function createLocPreviewItemTableColumns({ enabledFields = [] }) {
+  return [
+    {
+      prop: 'locId',
+      label: $t('pages.manager.locItem.table.locId'),
+      width: 110
+    },
+    {
+      prop: 'wareArea',
+      label: $t('pages.manager.locItem.table.wareArea'),
+      minWidth: 140,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'locCode',
+      label: $t('pages.manager.locItem.table.locCode'),
+      minWidth: 140,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'typeText',
+      label: $t('pages.manager.locItem.table.type'),
+      minWidth: 120,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'wkTypeText',
+      label: $t('pages.manager.locItem.table.wkType'),
+      minWidth: 120,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'orderId',
+      label: $t('pages.manager.locItem.table.orderId'),
+      minWidth: 120
+    },
+    {
+      prop: 'orderItemId',
+      label: $t('pages.manager.locItem.table.orderItemId'),
+      minWidth: 130
+    },
+    {
+      prop: 'matnrId',
+      label: $t('pages.manager.locItem.table.matnrId'),
+      minWidth: 110
+    },
+    {
+      prop: 'matnrCode',
+      label: $t('pages.manager.locItem.table.matnrCode'),
+      minWidth: 160,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'maktx',
+      label: $t('pages.manager.locItem.table.maktx'),
+      minWidth: 220,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'spec',
+      label: $t('pages.manager.locItem.table.spec'),
+      minWidth: 140,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'model',
+      label: $t('pages.manager.locItem.table.model'),
+      minWidth: 140,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'splrBatch',
+      label: $t('table.supplierBatch'),
+      minWidth: 140,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'batch',
+      label: $t('pages.manager.locItem.table.batch'),
+      minWidth: 140,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'trackCode',
+      label: $t('pages.manager.locItem.table.trackCode'),
+      minWidth: 150,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'unit',
+      label: $t('table.unit'),
+      width: 100
+    },
+    {
+      prop: 'anfme',
+      label: $t('pages.manager.locItem.table.anfme'),
+      width: 110,
+      align: 'right'
+    },
+    {
+      prop: 'qty',
+      label: $t('pages.manager.locItem.table.qty'),
+      width: 110,
+      align: 'right'
+    },
+    {
+      prop: 'workQty',
+      label: $t('pages.manager.locItem.table.workQty'),
+      width: 120,
+      align: 'right'
+    },
+    ...enabledFields.map((field) => ({
+      prop: field.prop,
+      label: field.label,
+      minWidth: 140,
+      showOverflowTooltip: true,
+      formatter: (row) => row[field.prop] || '-'
+    })),
+    {
+      prop: 'statusText',
+      label: $t('table.status'),
+      width: 90
+    },
+    {
+      prop: 'updateByText',
+      label: $t('table.updateBy'),
+      minWidth: 120,
+      showOverflowTooltip: true,
+      visible: false
+    },
+    {
+      prop: 'updateTimeText',
+      label: $t('table.updateTime'),
+      minWidth: 180,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'createByText',
+      label: $t('table.createBy'),
+      minWidth: 120,
+      showOverflowTooltip: true,
+      visible: false
+    },
+    {
+      prop: 'createTimeText',
+      label: $t('table.createTime'),
+      minWidth: 180,
+      showOverflowTooltip: true,
+      visible: false
+    },
+    {
+      prop: 'memo',
+      label: $t('table.memo'),
+      minWidth: 180,
+      showOverflowTooltip: true,
+      visible: false
+    }
+  ]
+}
diff --git a/rsf-design/src/views/manager/loc-preview/modules/loc-preview-items-page.vue b/rsf-design/src/views/manager/loc-preview/modules/loc-preview-items-page.vue
new file mode 100644
index 0000000..3a34fe8
--- /dev/null
+++ b/rsf-design/src/views/manager/loc-preview/modules/loc-preview-items-page.vue
@@ -0,0 +1,236 @@
+<template>
+  <div class="loc-item-page art-full-height">
+    <ArtSearchBar
+      v-model="searchForm"
+      :items="searchItems"
+      :showExpand="true"
+      @search="handleSearch"
+      @reset="handleReset"
+    />
+
+    <ElCard class="art-table-card">
+      <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+
+      <ArtTable
+        :loading="loading"
+        :data="data"
+        :columns="columns"
+        :pagination="pagination"
+        @pagination:size-change="handleSizeChange"
+        @pagination:current-change="handleCurrentChange"
+      />
+    </ElCard>
+  </div>
+</template>
+
+<script setup>
+  import { computed, onMounted, ref } from 'vue'
+  import { useRoute } from 'vue-router'
+  import { useI18n } from 'vue-i18n'
+  import { useTable } from '@/hooks/core/useTable'
+  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
+  import { useTableColumns } from '@/hooks/core/useTableColumns'
+  import { fetchEnabledFields, fetchLocItemPage } from '@/api/loc-item'
+  import { createLocPreviewItemTableColumns } from './loc-preview-item-table.columns'
+  import {
+    buildLocItemPageQueryParams,
+    buildLocItemSearchParams,
+    createLocItemSearchState,
+    getLocItemDynamicFieldKey,
+    getLocItemPaginationKey,
+    getLocItemStatusOptions,
+    normalizeLocItemEnabledFields,
+    normalizeLocItemRow
+  } from '@/views/manager/loc-item/locItemPage.helpers'
+
+  defineOptions({ name: 'LocPreviewItemsPage' })
+
+  const route = useRoute()
+  const { t } = useI18n()
+
+  const searchForm = ref(createLocItemSearchState())
+  const enabledFields = ref([])
+
+  const searchItems = computed(() => [
+    {
+      label: t('pages.manager.locItem.search.condition'),
+      key: 'condition',
+      type: 'input',
+      props: {
+        clearable: true,
+        placeholder: t('pages.manager.locItem.search.conditionPlaceholder')
+      }
+    },
+    {
+      label: t('pages.manager.locItem.search.timeStart'),
+      key: 'timeStart',
+      type: 'date',
+      props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
+    },
+    {
+      label: t('pages.manager.locItem.search.timeEnd'),
+      key: 'timeEnd',
+      type: 'date',
+      props: { clearable: true, valueFormat: 'YYYY-MM-DD', type: 'date' }
+    },
+    {
+      label: t('pages.manager.locItem.search.locId'),
+      key: 'locId',
+      type: 'inputNumber',
+      props: {
+        min: 0,
+        controlsPosition: 'right',
+        placeholder: t('pages.manager.locItem.search.locIdPlaceholder')
+      }
+    },
+    {
+      label: t('pages.manager.locItem.search.orderId'),
+      key: 'orderId',
+      type: 'inputNumber',
+      props: {
+        min: 0,
+        controlsPosition: 'right',
+        placeholder: t('pages.manager.locItem.search.orderIdPlaceholder')
+      }
+    },
+    {
+      label: t('pages.manager.locItem.search.type'),
+      key: 'type',
+      type: 'input',
+      props: { clearable: true, placeholder: t('pages.manager.locItem.search.typePlaceholder') }
+    },
+    {
+      label: t('pages.manager.locItem.search.wkType'),
+      key: 'wkType',
+      type: 'inputNumber',
+      props: {
+        min: 0,
+        controlsPosition: 'right',
+        placeholder: t('pages.manager.locItem.search.wkTypePlaceholder')
+      }
+    },
+    {
+      label: t('pages.manager.locItem.search.matnrCode'),
+      key: 'matnrCode',
+      type: 'input',
+      props: {
+        clearable: true,
+        placeholder: t('pages.manager.locItem.search.matnrCodePlaceholder')
+      }
+    },
+    {
+      label: t('pages.manager.locItem.search.maktx'),
+      key: 'maktx',
+      type: 'input',
+      props: { clearable: true, placeholder: t('pages.manager.locItem.search.maktxPlaceholder') }
+    },
+    {
+      label: t('pages.manager.locItem.search.trackCode'),
+      key: 'trackCode',
+      type: 'input',
+      props: {
+        clearable: true,
+        placeholder: t('pages.manager.locItem.search.trackCodePlaceholder')
+      }
+    },
+    {
+      label: t('pages.manager.locItem.search.batch'),
+      key: 'batch',
+      type: 'input',
+      props: { clearable: true, placeholder: t('pages.manager.locItem.search.batchPlaceholder') }
+    },
+    {
+      label: t('pages.manager.locItem.search.splrBatch'),
+      key: 'splrBatch',
+      type: 'input',
+      props: {
+        clearable: true,
+        placeholder: t('pages.manager.locItem.search.splrBatchPlaceholder')
+      }
+    },
+    {
+      label: t('table.status'),
+      key: 'status',
+      type: 'select',
+      props: { clearable: true, options: getLocItemStatusOptions() }
+    }
+  ])
+
+  const buildFieldConfigs = () =>
+    enabledFields.value.map((field) => ({
+      prop: getLocItemDynamicFieldKey(field.fields),
+      label: field.fieldsAlise
+    }))
+
+  const { columns, columnChecks, resetColumns } = useTableColumns(() =>
+    createLocPreviewItemTableColumns({
+      enabledFields: buildFieldConfigs()
+    })
+  )
+
+  const {
+    data,
+    loading,
+    pagination,
+    getData,
+    replaceSearchParams,
+    resetSearchParams,
+    handleSizeChange,
+    handleCurrentChange,
+    refreshData
+  } = useTable({
+    core: {
+      apiFn: (params) =>
+        guardRequestWithMessage(
+          fetchLocItemPage(params),
+          {
+            records: [],
+            total: 0,
+            current: params.current || 1,
+            pageSize: params.pageSize || params.size || 20
+          },
+          { timeoutMessage: t('pages.manager.locItem.messages.pageTimeout') }
+        ),
+      apiParams: buildLocItemPageQueryParams(searchForm.value),
+      immediate: false,
+      paginationKey: getLocItemPaginationKey()
+    },
+    transform: {
+      dataTransformer: (records) =>
+        Array.isArray(records)
+          ? records.map((item) => normalizeLocItemRow(item, enabledFields.value))
+          : []
+    }
+  })
+
+  async function loadEnabledFieldDefinitions() {
+    const fields = await guardRequestWithMessage(fetchEnabledFields(), [], {
+      timeoutMessage: t('pages.manager.locItem.messages.fieldsTimeout')
+    })
+    enabledFields.value = normalizeLocItemEnabledFields(fields)
+    resetColumns()
+  }
+
+  function handleSearch(params) {
+    replaceSearchParams(buildLocItemSearchParams(params))
+    getData()
+  }
+
+  function handleReset() {
+    const nextState = createLocItemSearchState()
+    if (route.query.locId) {
+      nextState.locId = route.query.locId
+    }
+    Object.assign(searchForm.value, nextState)
+    resetSearchParams()
+  }
+
+  onMounted(async () => {
+    if (route.query.locId) {
+      searchForm.value.locId = route.query.locId
+      replaceSearchParams(buildLocItemSearchParams(searchForm.value))
+    }
+    await loadEnabledFieldDefinitions()
+    await getData()
+  })
+</script>
diff --git a/rsf-design/src/views/stock/warehouse-stock/index.vue b/rsf-design/src/views/stock/warehouse-stock/index.vue
index e6ab489..d9e9fcb 100644
--- a/rsf-design/src/views/stock/warehouse-stock/index.vue
+++ b/rsf-design/src/views/stock/warehouse-stock/index.vue
@@ -9,7 +9,21 @@
     />
 
     <ElCard class="art-table-card">
-      <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" />
+      <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+        <template #left>
+          <ElSpace wrap>
+            <ElButton
+              v-auth="'list'"
+              :loading="exportLoading"
+              :disabled="loading || exportLoading || pagination.total === 0"
+              @click="handleExport"
+              v-ripple
+            >
+              瀵煎嚭
+            </ElButton>
+          </ElSpace>
+        </template>
+      </ArtTableHeader>
 
       <ArtTable
         :loading="loading"
@@ -35,6 +49,7 @@
 
     <WarehouseStockHistoriesDrawer
       v-model:visible="historiesDrawerVisible"
+      v-model:column-checks="historiesColumnChecks"
       :loading="historiesLoading"
       :summary="activeStockSummary"
       :data="historiesTableData"
@@ -49,6 +64,9 @@
 
 <script setup>
   import { computed, onMounted, reactive, ref } from 'vue'
+  import * as XLSX from 'xlsx'
+  import FileSaver from 'file-saver'
+  import { ElMessage } from 'element-plus'
   import { useTableColumns } from '@/hooks/core/useTableColumns'
   import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
   import {
@@ -78,6 +96,7 @@
 
   const searchForm = ref(createWarehouseStockSearchState())
   const loading = ref(false)
+  const exportLoading = ref(false)
   const tableData = ref([])
   const enabledFields = ref([])
   const activeStockSummary = ref({})
@@ -225,6 +244,34 @@
   function createHistoriesColumns() {
     return [
       {
+        prop: 'id',
+        label: 'ID',
+        width: 96,
+        align: 'center',
+        visible: false
+      },
+      {
+        prop: 'orderId',
+        label: '鍗曟嵁ID',
+        width: 100,
+        align: 'center',
+        visible: false
+      },
+      {
+        prop: 'sourceItemId',
+        label: '鏉ユ簮鏄庣粏ID',
+        width: 120,
+        align: 'center',
+        visible: false
+      },
+      {
+        prop: 'matnrId',
+        label: '鐗╂枡ID',
+        width: 100,
+        align: 'center',
+        visible: false
+      },
+      {
         prop: 'stockCode',
         label: '鍗曟嵁缂栧彿',
         minWidth: 180,
@@ -250,6 +297,34 @@
         formatter: (row) => row.batch || '-'
       },
       {
+        prop: 'splrName',
+        label: '渚涘簲鍟�',
+        minWidth: 180,
+        showOverflowTooltip: true,
+        formatter: (row) => row.splrName || '-'
+      },
+      {
+        prop: 'trackCode',
+        label: '璺熻釜鍙�',
+        minWidth: 160,
+        showOverflowTooltip: true,
+        formatter: (row) => row.trackCode || '-'
+      },
+      {
+        prop: 'prodTime',
+        label: '鐢熶骇鏃ユ湡',
+        minWidth: 140,
+        showOverflowTooltip: true,
+        formatter: (row) => row.prodTime || '-'
+      },
+      {
+        prop: 'packName',
+        label: '鍖呰',
+        minWidth: 140,
+        showOverflowTooltip: true,
+        formatter: (row) => row.packName || '-'
+      },
+      {
         prop: 'anfme',
         label: '搴撳瓨鏁伴噺',
         width: 120,
@@ -273,18 +348,74 @@
         width: 100,
         formatter: (row) => row.stockUnit || '-'
       },
+      {
+        prop: 'barcode',
+        label: '鏉$爜',
+        minWidth: 150,
+        showOverflowTooltip: true,
+        formatter: (row) => row.barcode || '-',
+        visible: false
+      },
+      {
+        prop: 'splrCode',
+        label: '渚涘簲鍟嗙紪鐮�',
+        minWidth: 140,
+        showOverflowTooltip: true,
+        formatter: (row) => row.splrCode || '-',
+        visible: false
+      },
+      {
+        prop: 'splrBatch',
+        label: '渚涘簲鍟嗘壒娆�',
+        minWidth: 150,
+        showOverflowTooltip: true,
+        formatter: (row) => row.splrBatch || '-',
+        visible: false
+      },
       ...createDynamicFieldColumns(),
+      {
+        prop: 'updateByText',
+        label: '鏇存柊浜�',
+        minWidth: 120,
+        formatter: (row) => row.updateByText || '-',
+        visible: false
+      },
+      {
+        prop: 'updateTimeText',
+        label: '鏇存柊鏃堕棿',
+        minWidth: 180,
+        formatter: (row) => row.updateTimeText || '-'
+      },
+      {
+        prop: 'createByText',
+        label: '鍒涘缓浜�',
+        minWidth: 120,
+        formatter: (row) => row.createByText || '-',
+        visible: false
+      },
       {
         prop: 'createTimeText',
         label: '鍒涘缓鏃堕棿',
         minWidth: 180,
         formatter: (row) => row.createTimeText || '-'
+      },
+      {
+        prop: 'memo',
+        label: '澶囨敞',
+        minWidth: 180,
+        showOverflowTooltip: true,
+        formatter: (row) => row.memo || '-',
+        visible: false
       }
     ]
   }
 
   const detailColumns = computed(() => createDetailColumns())
-  const historiesColumns = computed(() => createHistoriesColumns())
+  const {
+    columns: historiesColumns,
+    columnChecks: historiesColumnChecks,
+    resetColumns: resetHistoriesColumns
+  } = useTableColumns(() => createHistoriesColumns())
 
   function openDetailDrawer(row) {
     activeStockSummary.value = row
@@ -320,6 +451,7 @@
       }
     })
     resetColumns()
+    resetHistoriesColumns()
   }
 
   function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
@@ -382,9 +514,16 @@
       )
 
       detailTableData.value = Array.isArray(response?.records)
-        ? response.records.map((record) => normalizeWarehouseStockDetailRow(record, enabledFields.value))
+        ? response.records.map((record) =>
+            normalizeWarehouseStockDetailRow(record, enabledFields.value)
+          )
         : []
-      updatePaginationState(detailPagination, response, detailPagination.current, detailPagination.size)
+      updatePaginationState(
+        detailPagination,
+        response,
+        detailPagination.current,
+        detailPagination.size
+      )
     } finally {
       detailLoading.value = false
     }
@@ -416,7 +555,9 @@
       )
 
       historiesTableData.value = Array.isArray(response?.records)
-        ? response.records.map((record) => normalizeWarehouseStockHistoryRow(record, enabledFields.value))
+        ? response.records.map((record) =>
+            normalizeWarehouseStockHistoryRow(record, enabledFields.value)
+          )
         : []
       updatePaginationState(
         historiesPagination,
@@ -441,6 +582,76 @@
     await loadHistoriesData()
   }
 
+  function resolveExportColumns() {
+    return columns.value.filter((column) => column?.prop && column.prop !== 'operation')
+  }
+
+  function resolveExportCellValue(column, row) {
+    if (typeof column.formatter === 'function') {
+      const value = column.formatter(row)
+      return typeof value === 'string' || typeof value === 'number' ? value : ''
+    }
+    return row[column.prop] ?? ''
+  }
+
+  function buildExportRows(records, exportColumns) {
+    return records.map((row) =>
+      exportColumns.reduce((result, column) => {
+        result[column.label || column.prop] = resolveExportCellValue(column, row)
+        return result
+      }, {})
+    )
+  }
+
+  async function handleExport() {
+    exportLoading.value = true
+    try {
+      const exportSize = Number(pagination.total) > 0 ? Number(pagination.total) : 1000
+      const response = await guardRequestWithMessage(
+        fetchWarehouseStockPage(
+          buildWarehouseStockPageQueryParams({
+            ...searchForm.value,
+            current: 1,
+            pageSize: exportSize
+          })
+        ),
+        { records: [] },
+        {
+          timeoutMessage: '鍗虫椂搴撳瓨瀵煎嚭瓒呮椂锛屽凡鍋滄绛夊緟'
+        }
+      )
+      const records = Array.isArray(response?.records)
+        ? response.records.map((record) => normalizeWarehouseStockRow(record, enabledFields.value))
+        : []
+      if (records.length === 0) {
+        ElMessage.warning('鏆傛棤鍙鍑虹殑鍗虫椂搴撳瓨鏁版嵁')
+        return
+      }
+
+      const exportColumns = resolveExportColumns()
+      const exportRows = buildExportRows(records, exportColumns)
+      const worksheet = XLSX.utils.json_to_sheet(exportRows)
+      const workbook = XLSX.utils.book_new()
+      XLSX.utils.book_append_sheet(workbook, worksheet, '鍗虫椂搴撳瓨')
+      const excelBuffer = XLSX.write(workbook, {
+        bookType: 'xlsx',
+        type: 'array',
+        compression: true
+      })
+      FileSaver.saveAs(
+        new Blob([excelBuffer], {
+          type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+        }),
+        'warehouse-stock.xlsx'
+      )
+      ElMessage.success('瀵煎嚭鎴愬姛')
+    } catch (error) {
+      ElMessage.error(error?.message || '瀵煎嚭澶辫触')
+    } finally {
+      exportLoading.value = false
+    }
+  }
+
   function handleSearch(params) {
     searchForm.value = {
       ...searchForm.value,
diff --git a/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue b/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue
index 0c12fe9..2c43f61 100644
--- a/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue
+++ b/rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue
@@ -14,9 +14,12 @@
         <ElDescriptionsItem label="鎵规">{{ summary.batch || '--' }}</ElDescriptionsItem>
       </ElDescriptions>
 
-      <div class="flex justify-end">
-        <ElButton :loading="loading" @click="$emit('refresh')">鍒锋柊</ElButton>
-      </div>
+      <ArtTableHeader
+        :columns="columnChecks"
+        :loading="loading"
+        @update:columns="emit('update:columnChecks', $event)"
+        @refresh="$emit('refresh')"
+      />
 
       <ArtTable
         :loading="loading"
@@ -31,16 +34,25 @@
 </template>
 
 <script setup>
+  import ArtTableHeader from '@/components/core/tables/art-table-header/index.vue'
+
   defineProps({
     visible: { type: Boolean, default: false },
     loading: { type: Boolean, default: false },
     summary: { type: Object, default: () => ({}) },
     data: { type: Array, default: () => [] },
     columns: { type: Array, default: () => [] },
+    columnChecks: { type: Array, default: () => [] },
     pagination: { type: Object, default: () => ({ current: 1, size: 20, total: 0 }) }
   })
 
-  const emit = defineEmits(['update:visible', 'refresh', 'size-change', 'current-change'])
+  const emit = defineEmits([
+    'update:visible',
+    'update:columnChecks',
+    'refresh',
+    'size-change',
+    'current-change'
+  ])
 
   function handleVisibleChange(visible) {
     emit('update:visible', visible)
diff --git a/rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js b/rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js
index 09e27a6..6a607ac 100644
--- a/rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js
+++ b/rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js
@@ -1,5 +1,6 @@
 export const WAREHOUSE_STOCK_REPORT_TITLE = '鍗虫椂搴撳瓨鎶ヨ〃'
 export const WAREHOUSE_STOCK_DYNAMIC_FIELD_PREFIX = 'extendField__'
+export const DEFAULT_WAREHOUSE_STOCK_ORDER_BY = 'create_time desc'
 
 const AGG_TYPE_OPTIONS = [
   { label: '鎸夌墿鏂欐眹鎬�', value: 'matnr' },
@@ -93,6 +94,7 @@
   return {
     current: params.current || 1,
     pageSize: params.pageSize || params.size || 20,
+    orderBy: normalizeText(params.orderBy) || DEFAULT_WAREHOUSE_STOCK_ORDER_BY,
     ...buildWarehouseStockSearchParams(params)
   }
 }
@@ -111,12 +113,14 @@
     current: params.current || 1,
     pageSize: params.pageSize || params.size || 20,
     aggType: normalizeText(params.aggType) || 'matnr',
+    orderBy: normalizeText(params.orderBy) || DEFAULT_WAREHOUSE_STOCK_ORDER_BY,
     stock: params.stock || {}
   }
 }
 
 export function attachWarehouseStockDynamicFields(record = {}, enabledFields = []) {
-  const extendFields = record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
+  const extendFields =
+    record.extendFields && typeof record.extendFields === 'object' ? record.extendFields : {}
   const dynamicValues = {}
   enabledFields.forEach((field) => {
     dynamicValues[getWarehouseStockDynamicFieldKey(field.fields)] = extendFields[field.fields] || ''
@@ -131,6 +135,12 @@
   return attachWarehouseStockDynamicFields(
     {
       ...record,
+      id: normalizeNumber(record.id),
+      locId: normalizeNumber(record.locId),
+      locCode: record.locCode || '',
+      orderId: normalizeNumber(record.orderId),
+      orderItemId: normalizeNumber(record.orderItemId),
+      matnrId: normalizeNumber(record.matnrId),
       warehouseLabel: record['warehouse$'] || record.warehouse || '',
       matnrCode: record.matnrCode || '',
       maktx: record.maktx || '',
@@ -142,6 +152,7 @@
       anfme: normalizeNumber(record.anfme),
       qty: normalizeNumber(record.qty),
       workQty: normalizeNumber(record.workQty),
+      updateByText: record['updateBy$'] || record.updateBy || '',
       updateTimeText: record['updateTime$'] || record.updateTime || ''
     },
     enabledFields
@@ -170,14 +181,28 @@
   return attachWarehouseStockDynamicFields(
     {
       ...record,
+      id: normalizeNumber(record.id),
+      orderId: normalizeNumber(record.orderId),
+      sourceItemId: normalizeNumber(record.sourceItemId),
+      matnrId: normalizeNumber(record.matnrId),
       stockCode: record.stockCode || record.orderCode || '',
       orderCode: record.orderCode || '',
       matnrCode: record.matnrCode || '',
       maktx: record.maktx || '',
       batch: record.batch || '',
+      splrCode: record.splrCode || '',
+      splrBatch: record.splrBatch || '',
+      splrName: record.splrName || '',
+      trackCode: record.trackCode || '',
+      barcode: record.barcode || '',
+      prodTime: record.prodTime || '',
+      packName: record.packName || '',
       stockUnit: record.stockUnit || record.unit || '',
       qty: normalizeNumber(record.qty),
       workQty: normalizeNumber(record.workQty),
+      updateByText: record['updateBy$'] || record.updateBy || '',
+      createByText: record['createBy$'] || record.createBy || '',
+      memo: record.memo || '',
       createTimeText: record['createTime$'] || record.createTime || '',
       updateTimeText: record['updateTime$'] || record.updateTime || ''
     },
diff --git a/rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js b/rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js
index c69ed19..a5881f5 100644
--- a/rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js
+++ b/rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js
@@ -16,6 +16,49 @@
 
   return [
     {
+      prop: 'id',
+      label: 'ID',
+      width: 96,
+      align: 'center',
+      visible: false
+    },
+    {
+      prop: 'locId',
+      label: '搴撲綅ID',
+      width: 100,
+      align: 'center',
+      visible: false
+    },
+    {
+      prop: 'locCode',
+      label: '搴撲綅缂栫爜',
+      minWidth: 140,
+      showOverflowTooltip: true,
+      formatter: (row) => row.locCode || '-',
+      visible: false
+    },
+    {
+      prop: 'orderId',
+      label: '鍗曟嵁ID',
+      width: 100,
+      align: 'center',
+      visible: false
+    },
+    {
+      prop: 'orderItemId',
+      label: '鍗曟嵁鏄庣粏ID',
+      width: 120,
+      align: 'center',
+      visible: false
+    },
+    {
+      prop: 'matnrId',
+      label: '鐗╂枡ID',
+      width: 100,
+      align: 'center',
+      visible: false
+    },
+    {
       prop: 'matnrCode',
       label: '鐗╂枡缂栫爜',
       minWidth: 160,
@@ -46,6 +89,30 @@
       formatter: (row) => row.unit || '-'
     },
     {
+      prop: 'spec',
+      label: '瑙勬牸',
+      minWidth: 140,
+      showOverflowTooltip: true,
+      formatter: (row) => row.spec || '-',
+      visible: false
+    },
+    {
+      prop: 'model',
+      label: '鍨嬪彿',
+      minWidth: 140,
+      showOverflowTooltip: true,
+      formatter: (row) => row.model || '-',
+      visible: false
+    },
+    {
+      prop: 'fieldsIndex',
+      label: '鎵╁睍绱㈠紩',
+      minWidth: 160,
+      showOverflowTooltip: true,
+      formatter: (row) => row.fieldsIndex || '-',
+      visible: false
+    },
+    {
       prop: 'anfme',
       label: '鍙敤搴撳瓨',
       width: 120,
@@ -65,6 +132,13 @@
     },
     ...dynamicColumns,
     {
+      prop: 'updateByText',
+      label: '鏇存柊浜�',
+      minWidth: 110,
+      formatter: (row) => row.updateByText || '-',
+      visible: false
+    },
+    {
       prop: 'updateTimeText',
       label: '鏇存柊鏃堕棿',
       minWidth: 180,
@@ -73,18 +147,17 @@
     {
       prop: 'operation',
       label: '鍘嗗彶璁板綍',
-      width: 130,
+      width: 140,
       fixed: 'right',
       formatter: (row) =>
         h('div', { class: 'flex justify-end gap-2' }, [
           h(ArtButtonTable, {
             type: 'view',
-            text: '搴撳瓨璇︽儏',
             onClick: () => handleViewDetail(row)
           }),
           h(ArtButtonTable, {
-            type: 'view',
-            text: '鍘嗗彶璁板綍',
+            icon: 'ri:history-line',
+            iconClass: 'bg-warning/12 text-warning',
             onClick: () => handleViewHistories(row)
           })
         ])

--
Gitblit v1.9.1