فهرست منبع

SPA库存管理

Zimo 2 روز پیش
والد
کامیت
96fec57146
1فایلهای تغییر یافته به همراه486 افزوده شده و 504 حذف شده
  1. 486 504
      src/views/pms/iotsapstock/index.vue

+ 486 - 504
src/views/pms/iotsapstock/index.vue

@@ -1,235 +1,56 @@
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item :label="t('workOrderMaterial.factory')" prop="factoryId">
-        <el-select
-          v-model="queryParams.factoryId"
-          clearable
-          filterable
-          :placeholder="t('faultForm.choose')"
-          class="!w-240px"
-          @change="selectedFactoryChange"
-        >
-          <el-option
-            v-for="item in factoryList"
-            :key="item.id"
-            :label="item.factoryName"
-            :value="item.id!"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item
-        :label="t('workOrderMaterial.storageLocation')"
-        prop="storageLocationId"
-        style="margin-left: 28px"
-      >
-        <el-select
-          v-model="queryParams.storageLocationId"
-          clearable
-          filterable
-          :placeholder="t('faultForm.choose')"
-          class="!w-240px"
-        >
-          <el-option
-            v-for="item in storageLocationList"
-            :key="item.id"
-            :label="item.storageLocationName"
-            :value="item.id!"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
-        <el-input
-          v-model="queryParams.materialCode"
-          :placeholder="t('chooseMaintain.materialCode')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
-        <el-input
-          v-model="queryParams.materialName"
-          :placeholder="t('chooseMaintain.materialName')"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('chooseMaintain.createTime')" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          :start-placeholder="t('info.start')"
-          :end-placeholder="t('info.end')"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-220px"
-        />
-      </el-form-item>
-      <el-form-item :label="t('route.IsItConfigured')" prop="configFlag" label-width="140px">
-        <el-select
-          v-model="queryParams.configFlag"
-          :placeholder="t('faultForm.choose')"
-          clearable
-          class="!w-240px"
-        >
-          <el-option
-            v-for="dict in resultOptions"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"
-          ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
-        >
-        <el-button @click="resetQuery"
-          ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
-        >
-        <!-- v-hasPermi="['pms:iot-sap-stock:export']" -->
-        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-
-  <!-- ========== 统计信息卡片 ========== -->
-  <ContentWrap style="margin-bottom: 16px">
-    <el-card shadow="never" class="stat-card">
-      <div class="stat-container">
-        <div class="stat-item">
-          <span class="stat-label">{{ t('stock.totalQuantity') }}:</span>
-          <span class="stat-value">{{ totalQuantity.toLocaleString() }}</span>
-        </div>
-        <div class="stat-item">
-          <span class="stat-label">{{ t('stock.totalAmount') }}:</span>
-          <span class="stat-value"
-            >¥
-            {{
-              totalAmount.toLocaleString(undefined, {
-                minimumFractionDigits: 2,
-                maximumFractionDigits: 2
-              })
-            }}</span
-          >
-        </div>
-      </div>
-    </el-card>
-  </ContentWrap>
-
-  <!-- 列表 -->
-  <ContentWrap ref="tableContainerRef" class="table-container">
-    <el-table
-      ref="tableRef"
-      v-loading="loading"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="false"
-      style="width: 100%"
-    >
-      <el-table-column
-        :label="t('workOrderMaterial.factory')"
-        align="center"
-        prop="factory"
-        :width="columnWidths.factory"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.storageLocation')"
-        align="center"
-        prop="projectDepartment"
-        :width="columnWidths.projectDepartment"
-      />
-      <el-table-column
-        :label="t('chooseMaintain.materialCode')"
-        align="center"
-        prop="materialCode"
-        :width="columnWidths.materialCode"
-      />
-      <el-table-column
-        :label="t('chooseMaintain.materialName')"
-        align="left"
-        prop="materialName"
-        :width="columnWidths.materialName"
-      />
-      <el-table-column
-        :label="t('route.quantity')"
-        align="center"
-        prop="quantity"
-        :width="columnWidths.quantity"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.unitPrice')"
-        align="center"
-        prop="unitPrice"
-        :width="columnWidths.unitPrice"
-      />
-      <el-table-column
-        :label="t('workOrderMaterial.unit')"
-        align="center"
-        prop="unit"
-        :width="columnWidths.unit"
-      />
-      <el-table-column
-        :label="t('route.safetyStock')"
-        align="center"
-        prop="safetyStock"
-        :width="columnWidths.safetyStock"
-      />
-      <el-table-column
-        :label="t('chooseMaintain.createTime')"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        :width="columnWidths.createTime"
-      />
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
-
-  <!-- 表单弹窗:添加/修改 -->
-  <IotSapStockForm ref="formRef" @success="getList" />
-</template>
-
 <script setup lang="ts">
-import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotSapStockApi, IotSapStockVO } from '@/api/pms/iotsapstock'
-import IotSapStockForm from './IotSapStockForm.vue'
 import * as SapOrgApi from '@/api/system/saporg'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import CountTo from '@/components/count-to1.vue'
 
-/** PMS SAP 库存(通用库存/项目部库存) 列表 */
 defineOptions({ name: 'IotSapStock' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+type StockRow = IotSapStockVO & {
+  totalQuantity?: number | string
+  totalAmount?: number | string
+  createTime?: string
+}
 
-const factoryList = ref([] as SapOrgApi.SapOrgVO[]) // 工厂列表
-const storageLocationList = ref([] as SapOrgApi.SapOrgVO[]) // 库存地点列表
-// 新增统计变量
-const totalQuantity = ref(0) // 总数量
-const totalAmount = ref(0) // 总金额
+interface QueryParams extends PageParam {
+  deptId?: number
+  factoryId?: number
+  factory?: string
+  storageLocationId?: number
+  projectDepartment?: string
+  materialCode?: string
+  materialName?: string
+  materialGroupName?: string
+  materialGroupId?: number
+  quantity?: number
+  unitPrice?: number
+  unit?: string
+  safetyStock?: number
+  shelvesId?: number
+  cargoLocationId?: number
+  type?: number
+  syncStatus?: number
+  syncTime?: string[]
+  syncError?: string
+  sort?: number
+  status?: number
+  remark?: string
+  configFlag?: string
+  createTime?: string[]
+}
 
-const loading = ref(true) // 列表的加载中
-const list = ref<IotSapStockVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+interface SelectedFactoryReqVO {
+  type: number
+  factoryCodes: string[]
+}
+
+const { t } = useI18n()
+const message = useMessage()
+const { ZmTable, ZmTableColumn } = useTableComponents<StockRow>()
+
+const initQuery: QueryParams = {
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -256,363 +77,524 @@ const queryParams = reactive({
   remark: undefined,
   configFlag: 'A',
   createTime: []
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
-
-// 表格容器和表格的引用
-const tableContainerRef = ref()
-const tableRef = ref()
-
-// 列宽度配置
-const columnWidths = ref({
-  factory: '120px',
-  projectDepartment: '120px',
-  materialCode: '120px',
-  materialName: '200px', // 初始宽度,会被计算覆盖
-  quantity: '100px',
-  unitPrice: '100px',
-  unit: '100px',
-  safetyStock: '120px',
-  createTime: '180px'
-})
+}
 
-/** 获取滚动条宽度 */
-const getScrollbarWidth = () => {
-  const outer = document.createElement('div')
-  outer.style.visibility = 'hidden'
-  outer.style.overflow = 'scroll'
-  document.body.appendChild(outer)
-
-  const inner = document.createElement('div')
-  outer.appendChild(inner)
-
-  const scrollbarWidth = outer.offsetWidth - inner.offsetWidth
-  outer.parentNode?.removeChild(outer)
-
-  return scrollbarWidth
-}
-
-/** 计算文本宽度 */
-const getTextWidth = (text: string, fontSize = 14) => {
-  const span = document.createElement('span')
-  span.style.visibility = 'hidden'
-  span.style.position = 'absolute'
-  span.style.whiteSpace = 'nowrap'
-  span.style.fontSize = `${fontSize}px`
-  span.style.fontFamily = 'inherit'
-  span.innerText = text
-
-  document.body.appendChild(span)
-  const width = span.offsetWidth
-  document.body.removeChild(span)
-
-  return width
-}
-
-/** 计算列宽度 */
-const calculateColumnWidths = () => {
-  const MIN_WIDTH = 80 // 最小列宽
-  const PADDING = 25 // 列内边距
-  const FLEXIBLE_COLUMN = 'materialName' // 可伸缩列
-  const scrollbarWidth = getScrollbarWidth() // 动态获取滚动条宽度
-
-  if (!tableContainerRef.value?.$el || list.value.length === 0) return
-
-  const containerWidth = tableContainerRef.value.$el.clientWidth
-
-  // 需要自适应的列配置
-  const autoColumns = [
-    { key: 'factory', label: t('workOrderMaterial.factory'), getValue: (row) => row.factory },
-    {
-      key: 'projectDepartment',
-      label: t('workOrderMaterial.storageLocation'),
-      getValue: (row) => row.projectDepartment
-    },
-    {
-      key: 'materialCode',
-      label: t('chooseMaintain.materialCode'),
-      getValue: (row) => row.materialCode
-    },
-    {
-      key: 'materialName',
-      label: t('chooseMaintain.materialName'),
-      getValue: (row) => row.materialName
-    },
-    { key: 'quantity', label: t('route.quantity'), getValue: (row) => String(row.quantity) },
-    {
-      key: 'unitPrice',
-      label: t('workOrderMaterial.unitPrice'),
-      getValue: (row) => String(row.unitPrice)
-    },
-    { key: 'unit', label: t('workOrderMaterial.unit'), getValue: (row) => row.unit },
-    {
-      key: 'safetyStock',
-      label: t('route.safetyStock'),
-      getValue: (row) => String(row.safetyStock)
-    },
-    {
-      key: 'createTime',
-      label: t('chooseMaintain.createTime'),
-      getValue: (row) => dateFormatter(null, null, row.createTime)
-    }
-  ]
-
-  const newWidths: Record<string, string> = {}
-  let totalFixedWidth = 0 // 所有固定列的总宽度
-
-  // 计算除可伸缩列外的所有列宽度
-  autoColumns.forEach((col) => {
-    if (col.key === FLEXIBLE_COLUMN) return
-
-    const headerText = col.label
-    const headerWidth = getTextWidth(headerText) * 1.3 // 表头宽度(加粗效果增加30%)
-
-    // 计算内容最大宽度
-    let contentMaxWidth = 0
-    list.value.forEach((row) => {
-      const text = col.getValue ? String(col.getValue(row)) : String(row[col.key] || '')
-      const textWidth = getTextWidth(text)
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
-    })
-
-    // 取表头宽度、内容最大宽度和最小宽度的最大值
-    const finalWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
-    newWidths[col.key] = `${finalWidth}px`
-    totalFixedWidth += finalWidth
-  })
-
-  // 处理可伸缩列(materialName)
-  const flexibleCol = autoColumns.find((col) => col.key === FLEXIBLE_COLUMN)
-  if (flexibleCol) {
-    const headerText = flexibleCol.label
-    const headerWidth = getTextWidth(headerText) * 1.3
-
-    let contentMaxWidth = 0
-    list.value.forEach((row) => {
-      const text = flexibleCol.getValue
-        ? String(flexibleCol.getValue(row))
-        : String(row[flexibleCol.key] || '')
-      const textWidth = getTextWidth(text)
-      if (textWidth > contentMaxWidth) contentMaxWidth = textWidth
-    })
-
-    const baseWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING
-
-    // 剩余空间 = 容器宽度 - 其他列总宽度 - 垂直滚动条宽度
-    const remainingWidth = containerWidth - totalFixedWidth - scrollbarWidth
-
-    // 可伸缩列的宽度取剩余空间和基础宽度的最大值
-    const flexibleWidth = Math.max(remainingWidth, baseWidth)
-    newWidths[FLEXIBLE_COLUMN] = `${flexibleWidth}px`
-  }
+const queryParams = reactive<QueryParams>({ ...initQuery })
+const queryFormRef = ref()
+const loading = ref(false)
+const exportLoading = ref(false)
+const list = ref<StockRow[]>([])
+const total = ref(0)
+const totalQuantity = ref(0)
+const totalAmount = ref(0)
+const factoryList = ref<SapOrgApi.SapOrgVO[]>([])
+const storageLocationList = ref<SapOrgApi.SapOrgVO[]>([])
 
-  // 更新列宽度
-  columnWidths.value = newWidths
+const resultOptions = computed(() => [
+  { label: '全部', value: 'A' },
+  { label: '是', value: 'Y' },
+  { label: '否', value: 'N' }
+])
 
-  // 重新布局表格
-  nextTick(() => {
-    tableRef.value?.doLayout()
-  })
-}
+const stockStatCards = computed(() => [
+  {
+    label: t('stock.totalQuantity'),
+    value: totalQuantity.value,
+    prefix: '',
+    unit: '',
+    decimals: 0,
+    icon: 'i-tabler:packages text-sky'
+  },
+  {
+    label: t('stock.totalAmount'),
+    value: totalAmount.value,
+    prefix: '¥',
+    unit: '',
+    decimals: 2,
+    icon: 'i-material-symbols:payments-outline-rounded text-emerald'
+  }
+])
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
     const data = await IotSapStockApi.getIotSapStockPage(queryParams)
     list.value = data.list
     total.value = data.total
-    // 从第一条记录中提取统计值
-    if (data.list && data.list.length > 0) {
-      // 确保取到有效的数值(第一条记录中的统计值代表整个查询结果)
-      totalQuantity.value = Number(data.list[0].totalQuantity) || 0
-      totalAmount.value = Number(data.list[0].totalAmount) || 0
-    } else {
-      // 没有数据时重置为0
-      totalQuantity.value = 0
-      totalAmount.value = 0
-    }
-    // 数据加载完成后计算列宽
-    nextTick(() => {
-      calculateColumnWidths()
-    })
+
+    const firstRow = data.list?.[0]
+    totalQuantity.value = Number(firstRow?.totalQuantity) || 0
+    totalAmount.value = Number(firstRow?.totalAmount) || 0
   } finally {
     loading.value = false
   }
-  // 加载工厂(SAP)列表
-  factoryList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(1)
-  // 加载库存地点(SAP)列表
-  storageLocationList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
 }
 
-/** 搜索按钮操作 */
+const loadSapOrgOptions = async () => {
+  const [factories, storageLocations] = await Promise.all([
+    SapOrgApi.SapOrgApi.getSimpleSapOrgList(1),
+    SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
+  ])
+  factoryList.value = factories
+  storageLocationList.value = storageLocations
+}
+
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  Object.assign(queryParams, { ...initQuery })
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
 }
 
-const selectedFactoryReqVO = ref({
-  type: 0, // 类型(1工厂 2成本中心 3库位)
-  factoryCodes: [] // 已经选择的SAP工厂code 列表
-})
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
 
-/** 已经选择了 SAP工厂 */
 const selectedFactoryChange = async (selectedId: number | undefined) => {
-  // 获取选中的factoryCode数组
-  const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
-  const selectedFactoryCodes = selectedFactory ? [selectedFactory.factoryCode] : []
-
-  // 获得已经选择的 SAP 工厂 数组
-  // const factoryIds = formData.value.factoryIds
-  console.log('选择的工厂代码:', selectedFactoryCodes)
-  // 根据选择的 SAP工厂 调用后台接口查询 SAP工厂下属的 库存地点列表
-  selectedFactoryReqVO.value.type = 3
-  selectedFactoryReqVO.value.factoryCodes = selectedFactoryCodes
-  storageLocationList.value = await SapOrgApi.SapOrgApi.getSelectedList(selectedFactoryReqVO.value)
-}
+  queryParams.storageLocationId = undefined
 
-// 是否已经配置了安全库存 下拉列表 模拟字典项
-const resultOptions = computed(() => [
-  {
-    label: '全部',
-    value: 'A' // 空值会触发 clearable 效果
-  },
-  {
-    label: '是',
-    value: 'Y' // 空值会触发 clearable 效果
-  },
-  {
-    label: '否',
-    value: 'N' // 空值会触发 clearable 效果
+  if (!selectedId) {
+    storageLocationList.value = await SapOrgApi.SapOrgApi.getSimpleSapOrgList(3)
+    return
   }
-])
 
-/** 删除按钮操作 */
-const handleDelete = async (id: number) => {
-  try {
-    // 删除的二次确认
-    await message.delConfirm()
-    // 发起删除
-    await IotSapStockApi.deleteIotSapStock(id)
-    message.success(t('common.delSuccess'))
-    // 刷新列表
-    await getList()
-  } catch {}
+  const selectedFactory = factoryList.value.find((item) => item.id === selectedId)
+  const selectedFactoryReqVO: SelectedFactoryReqVO = {
+    type: 3,
+    factoryCodes: selectedFactory?.factoryCode ? [selectedFactory.factoryCode] : []
+  }
+  storageLocationList.value = await SapOrgApi.SapOrgApi.getSelectedList(selectedFactoryReqVO)
 }
 
-/** 导出按钮操作 */
 const handleExport = async () => {
   try {
     exportLoading.value = true
     const data = await IotSapStockApi.exportIotSapStock(queryParams)
     download.excel(data, 'SPA库存.xls')
   } catch {
+    message.error('导出失败')
   } finally {
     exportLoading.value = false
   }
 }
 
-/** 初始化 **/
 onMounted(() => {
+  loadSapOrgOptions()
   getList()
-  // 添加窗口大小变化监听
-  window.addEventListener('resize', calculateColumnWidths)
 })
+</script>
 
-onUnmounted(() => {
-  // 移除窗口大小变化监听
-  window.removeEventListener('resize', calculateColumnWidths)
-})
+<template>
+  <div
+    class="iot-sap-stock-page grid grid-rows-[auto_auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+  >
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="82px"
+      class="iot-sap-stock-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0"
+    >
+      <div class="query-row">
+        <el-form-item :label="t('workOrderMaterial.factory')" prop="factoryId">
+          <el-select
+            v-model="queryParams.factoryId"
+            clearable
+            filterable
+            :placeholder="t('faultForm.choose')"
+            class="query-control"
+            @change="selectedFactoryChange"
+          >
+            <el-option
+              v-for="item in factoryList"
+              :key="item.id"
+              :label="item.factoryName"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('workOrderMaterial.storageLocation')" prop="storageLocationId">
+          <el-select
+            v-model="queryParams.storageLocationId"
+            clearable
+            filterable
+            :placeholder="t('faultForm.choose')"
+            class="query-control"
+          >
+            <el-option
+              v-for="item in storageLocationList"
+              :key="item.id"
+              :label="item.storageLocationName"
+              :value="item.id!"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.materialCode')" prop="materialCode">
+          <el-input
+            v-model="queryParams.materialCode"
+            :placeholder="t('chooseMaintain.materialCode')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.materialName')" prop="materialName">
+          <el-input
+            v-model="queryParams.materialName"
+            :placeholder="t('chooseMaintain.materialName')"
+            clearable
+            class="query-control"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('chooseMaintain.createTime')" prop="createTime">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            :start-placeholder="t('info.start')"
+            :end-placeholder="t('info.end')"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="query-control query-control--date"
+          />
+        </el-form-item>
+        <el-form-item :label="t('route.IsItConfigured')" prop="configFlag" label-width="116px">
+          <el-select
+            v-model="queryParams.configFlag"
+            :placeholder="t('faultForm.choose')"
+            clearable
+            class="query-control query-control--small"
+          >
+            <el-option
+              v-for="dict in resultOptions"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      </div>
 
-// 监听列表数据变化,重新计算列宽
-watch(
-  list,
-  () => {
-    nextTick(calculateColumnWidths)
-  },
-  { deep: true }
-)
-</script>
+      <div class="query-side">
+        <el-form-item class="query-actions">
+          <el-button type="primary" @click="handleQuery">
+            <Icon icon="ep:search" class="mr-5px" />{{ t('operationFill.search') }}
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" />{{ t('operationFill.reset') }}
+          </el-button>
+          <el-button type="success" plain :loading="exportLoading" @click="handleExport">
+            <Icon icon="ep:download" class="mr-5px" />导出
+          </el-button>
+        </el-form-item>
+      </div>
+    </el-form>
+
+    <div class="stock-stat-grid">
+      <div v-for="item in stockStatCards" :key="item.label" class="stock-stat-card group">
+        <div class="relative z-10 flex h-full flex-col justify-center">
+          <span class="stat-label">{{ item.label }}</span>
+          <div class="flex items-baseline gap-1">
+            <span v-if="item.prefix" class="text-[12px] text-gray-400">{{ item.prefix }}</span>
+            <count-to
+              class="stat-value"
+              :start-val="0"
+              :end-val="item.value"
+              :decimals="item.decimals"
+            />
+            <span v-if="item.unit" class="stat-unit">{{ item.unit }}</span>
+          </div>
+        </div>
+        <div class="stat-icon">
+          <div :class="item.icon" class="text-5xl"></div>
+        </div>
+      </div>
+    </div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border
+            >
+              <ZmTableColumn
+                type="index"
+                :label="t('monitor.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings
+              />
+              <ZmTableColumn
+                prop="factory"
+                :label="t('workOrderMaterial.factory')"
+                min-width="120"
+              />
+              <ZmTableColumn
+                prop="projectDepartment"
+                :label="t('workOrderMaterial.storageLocation')"
+                min-width="140"
+              />
+              <ZmTableColumn
+                prop="materialCode"
+                :label="t('chooseMaintain.materialCode')"
+                min-width="150"
+              />
+              <ZmTableColumn
+                prop="materialName"
+                :label="t('chooseMaintain.materialName')"
+                min-width="220"
+                align="left"
+              />
+              <ZmTableColumn prop="quantity" :label="t('route.quantity')" min-width="100" />
+              <ZmTableColumn
+                prop="unitPrice"
+                :label="t('workOrderMaterial.unitPrice')"
+                min-width="100"
+              />
+              <ZmTableColumn prop="unit" :label="t('workOrderMaterial.unit')" min-width="90" />
+              <ZmTableColumn prop="safetyStock" :label="t('route.safetyStock')" min-width="120" />
+              <ZmTableColumn
+                prop="createTime"
+                :label="t('chooseMaintain.createTime')"
+                min-width="180"
+              >
+                <template #default="{ row }">
+                  {{ dateFormatter(row, null, row.createTime) }}
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+  </div>
+</template>
 
 <style scoped>
-/* 统计卡片样式 */
-.stat-card {
-  border-radius: 4px;
-  border: 1px solid #ebeef5;
+.iot-sap-stock-query {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px 24px;
 }
 
-.stat-container {
+.query-row {
   display: flex;
-  padding: 1px;
+  flex: 1 1 auto;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 12px 22px;
+  min-width: 0;
 }
 
-.stat-item {
+.query-side {
   display: flex;
+  flex: 0 0 auto;
+  flex-wrap: wrap;
   align-items: center;
-  margin-right: 40px; /* 控制项间距 */
+  justify-content: flex-end;
+  gap: 12px 18px;
 }
 
 .stat-label {
-  font-weight: bold;
-  color: #606266;
-  margin-right: 8px;
+  margin-bottom: 2px;
+  font-size: 11px;
+  font-weight: 500;
+  color: var(--el-text-color-regular);
+  letter-spacing: 0;
 }
 
 .stat-value {
+  font-family: var(--el-font-family);
   font-size: 18px;
-  font-weight: bold;
-  color: #409eff;
+  font-weight: 700;
+  line-height: 1;
+  color: var(--el-text-color-primary);
+}
+
+.stat-unit {
+  font-size: 10px;
+  font-weight: 400;
+  color: var(--el-text-color-secondary);
 }
 
-/* 表格容器样式 - 确保可以水平滚动 */
-.table-container {
-  overflow-x: auto;
+.stock-stat-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
 }
 
-/* 防止表格内容换行 */
-:deep(.el-table) .cell {
-  white-space: nowrap !important;
-  overflow: visible !important;
-  text-overflow: unset !important;
+.stock-stat-card {
+  position: relative;
+  min-height: 78px;
+  padding: 10px 14px;
+  overflow: hidden;
+  background-color: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-extra-light);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-lighter);
+  transition:
+    border-color 0.3s,
+    box-shadow 0.3s;
 }
 
-/* 确保表格行不换行 */
-:deep(.el-table__row) {
-  white-space: nowrap;
+.stock-stat-card:hover {
+  border-color: var(--el-color-primary-light-7);
+  box-shadow: var(--el-box-shadow-light);
+}
+
+.stat-icon {
+  position: absolute;
+  right: -8px;
+  bottom: -12px;
+  opacity: 0.4;
+  transform: rotate(-10deg);
+  transition:
+    transform 0.5s,
+    opacity 0.5s;
+}
+
+.stock-stat-card:hover .stat-icon {
+  opacity: 0.55;
+  transform: scale(1.08) rotate(0);
+}
+
+.query-actions {
+  flex: 0 0 auto;
+}
+
+.query-actions :deep(.el-form-item__content) {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 10px;
 }
 
-/* 表头特别处理 */
-:deep(.el-table__header) {
-  .cell {
-    display: inline-block;
-    white-space: nowrap;
-    width: auto !important;
+.query-actions :deep(.el-button) {
+  margin-left: 0;
+}
+
+.query-control {
+  width: 190px;
+}
+
+.query-control--small {
+  width: 140px;
+}
+
+.query-control--date {
+  width: 220px;
+}
+
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width >= 2400px) {
+  .iot-sap-stock-query,
+  .query-row,
+  .query-side {
+    flex-wrap: nowrap;
   }
 }
 
-/* 表格整体布局优化 */
-:deep(.el-table__inner-wrapper) {
-  min-width: 100% !important;
-  width: auto !important;
+@media (width <= 1500px) {
+  .iot-sap-stock-query,
+  .query-row {
+    gap: 12px 18px;
+  }
+
+  .query-control {
+    width: 176px;
+  }
+
+  .query-control--small {
+    width: 132px;
+  }
+
+  .query-control--date {
+    width: 210px;
+  }
 }
 
-/* 单元格内容完全显示 */
-:deep(.el-table__body-wrapper) .el-table__cell .cell {
-  display: block;
-  overflow: visible;
-  text-overflow: unset;
+@media (width <= 1200px) {
+  .iot-sap-stock-page {
+    grid-template-rows: auto auto minmax(480px, 1fr);
+    height: auto;
+    min-height: calc(
+      100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+    );
+  }
+
+  .query-side {
+    width: 100%;
+    justify-content: space-between;
+  }
+}
+
+@media (width <= 768px) {
+  .iot-sap-stock-query {
+    padding: 12px;
+  }
+
+  .query-row,
+  .query-row :deep(.el-form-item),
+  .query-side,
+  .query-actions {
+    width: 100%;
+  }
+
+  .query-control,
+  .query-control--small,
+  .query-control--date {
+    width: 100%;
+  }
+
+  .stock-stat-grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .query-actions :deep(.el-form-item__content) {
+    display: grid;
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+    gap: 8px;
+    width: 100%;
+  }
+
+  .query-actions :deep(.el-button) {
+    width: 100%;
+    margin-left: 0;
+  }
 }
 </style>