zhou zhou
18 小时以前 adb016e4492d927ed3eb9fc098294ffc81c06ae3
#页面优化
2个文件已添加
6个文件已修改
1094 ■■■■■ 已修改文件
rsf-design/src/api/warehouse-stock.js 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/loc-preview/index.vue 330 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/loc-preview/modules/loc-preview-item-table.columns.js 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/manager/loc-preview/modules/loc-preview-items-page.vue 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/stock/warehouse-stock/index.vue 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/stock/warehouse-stock/modules/warehouse-stock-histories-drawer.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/stock/warehouse-stock/warehouseStockPage.helpers.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/stock/warehouse-stock/warehouseStockTable.columns.js 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 = {}) {
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>
rsf-design/src/views/manager/loc-preview/modules/loc-preview-item-table.columns.js
New file
@@ -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
    }
  ]
}
rsf-design/src/views/manager/loc-preview/modules/loc-preview-items-page.vue
New file
@@ -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>
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,
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)
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 || ''
    },
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)
          })
        ])