Bladeren bron

feat: 优化 BOM 物料选择与详情交互

- 重构物料选择弹窗样式,改用 ZmTable 和 Element Plus 原生多选
- 移除已选物料标签区域,简化跨页选择和数量同步逻辑
- 优化物料详情抽屉关闭逻辑,避免关闭时刷新父表导致闪烁
Zimo 1 week geleden
bovenliggende
commit
9b2fbf329d
2 gewijzigde bestanden met toevoegingen van 289 en 422 verwijderingen
  1. 288 410
      src/views/pms/bom/MaterialList.vue
  2. 1 12
      src/views/pms/bom/MaterialListDrawer.vue

+ 288 - 410
src/views/pms/bom/MaterialList.vue

@@ -1,501 +1,379 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="t('iotMaintain.selectMaterials')"
-          style="width: 1300px; min-height: 300px" :close-on-click-modal="false" @close="handleClose">
-
-    <!-- 设备分类和BOM节点信息显示 -->
-    <div style="margin: 0 0px 15px; background: #f5f7fa; padding: 12px 20px; border-radius: 4px;">
-      <el-row>
-        <el-col :span="12">
-          <span style="font-weight: bold; margin-right: 10px;">设备分类:</span>
-          <span>{{ deviceCategoryName }}</span>
-        </el-col>
-        <el-col :span="12">
-          <span style="font-weight: bold; margin-right: 10px;">BOM节点:</span>
-          <span>{{ bomNodeName }}</span>
-        </el-col>
-      </el-row>
-    </div>
-
-    <ContentWrap>
-      <el-form
-        class="-mb-15px"
-        :model="queryParams"
-        ref="queryFormRef"
-        :inline="true"
-        label-width="68px"
-      >
-        <el-form-item :label="t('workOrderMaterial.materialName')" prop="name" style="margin-left: 28px">
-          <el-input
-            v-model="queryParams.name"
-            :placeholder="t('workOrderMaterial.nameHolder')"
-            clearable
-            @keyup.enter="handleQuery"
-            class="!w-200px"
-          />
-        </el-form-item>
-        <el-form-item :label="t('workOrderMaterial.materialCode')" prop="code">
-          <el-input
-            v-model="queryParams.code"
-            :placeholder="t('workOrderMaterial.codeHolder')"
-            clearable
-            @keyup.enter="handleQuery"
-            class="!w-200px"
-          />
-        </el-form-item>
-        <el-form-item>
-          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />
-            {{ t('workOrderMaterial.search') }}</el-button>
-          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />
-            {{ t('workOrderMaterial.reset') }}</el-button>
-          <el-button @click="handleConfirm" class="custom-green-button"><Icon icon="ep:check" class="mr-5px" /> {{ t('workOrderMaterial.confirm') }}</el-button>
-        </el-form-item>
-      </el-form>
-    </ContentWrap>
-
-    <!-- 已选物料标签区域 -->
-    <ContentWrap v-if="selectedRows.length > 0"
-                 style="margin: -10px 0px 10px; padding: 10px 15px; background: #f8fafc; border-radius: 4px; border: 1px solid #ebeef5;">
-      <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 8px;">
-        <div style="font-weight: bold; color: #606266; margin-right: 10px;">已选物料:</div>
-        <el-tag
-          v-for="item in selectedRows"
-          :key="item.id"
-          @close="removeTag(item)"
-          style="margin-bottom: 5px; position: relative; padding-right: 25px;"
-        >
-          {{ item.name }}-{{ item.code }}
-          <!-- 自定义关闭按钮 -->
-          <span class="close-icon" @click.stop="removeTag(item)">
-            <Icon icon="ep:close" style="font-size: 12px; position: absolute; right: 8px; top: 50%; transform: translateY(-50%);"/>
-          </span>
-        </el-tag>
-      </div>
-    </ContentWrap>
-
-    <ContentWrap class="table-content-wrap">
-      <div class="flex-table-container">
-        <el-table
+  <Dialog
+    v-model="dialogVisible"
+    :title="t('iotMaintain.selectMaterials')"
+    width="1200px"
+    :close-on-click-modal="false"
+    @close="handleClose">
+    <div class="material-picker">
+      <section class="context-panel">
+        <div class="context-item">
+          <span class="context-label">设备分类</span>
+          <span class="context-value">{{ deviceCategoryName || '暂无' }}</span>
+        </div>
+        <div class="context-item">
+          <span class="context-label">BOM节点</span>
+          <span class="context-value">{{ bomNodeName || '暂无' }}</span>
+        </div>
+      </section>
+
+      <section class="filter-panel">
+        <el-form
+          ref="queryFormRef"
+          :model="queryParams"
+          :inline="true"
+          label-width="78px"
+          size="default"
+          class="filter-form">
+          <el-form-item :label="t('workOrderMaterial.materialName')" prop="name">
+            <el-input
+              v-model="queryParams.name"
+              :placeholder="t('workOrderMaterial.nameHolder')"
+              clearable
+              class="filter-input"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item :label="t('workOrderMaterial.materialCode')" prop="code">
+            <el-input
+              v-model="queryParams.code"
+              :placeholder="t('workOrderMaterial.codeHolder')"
+              clearable
+              class="filter-input"
+              @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item class="filter-actions">
+            <el-button type="primary" size="default" @click="handleQuery">
+              <Icon icon="ep:search" class="mr-5px" />
+              {{ t('workOrderMaterial.search') }}
+            </el-button>
+            <el-button size="default" @click="resetQuery">
+              <Icon icon="ep:refresh" class="mr-5px" />
+              {{ t('workOrderMaterial.reset') }}
+            </el-button>
+            <el-button type="success" size="default" @click="handleConfirm">
+              <Icon icon="ep:check" class="mr-5px" />
+              {{ t('workOrderMaterial.confirm') }}
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </section>
+
+      <section class="table-panel">
+        <ZmTable
           ref="tableRef"
-          v-loading="loading"
-                  :data="list"
-                  :stripe="true"
-                  :row-key="rowKey"
-                  :show-overflow-tooltip="true"
-                  table-layout="auto"
-                  @row-click="handleRowClick"
-                  class="full-width-table" >
-          <el-table-column :label="t('workOrderMaterial.select')" class-name="select-column" min-width="30">
+          :data="list"
+          :loading="loading"
+          :show-border="true"
+          class="material-table"
+          settings-cache-key="pms-bom-material-select"
+          row-key="id"
+          @row-click="handleRowClick"
+          @selection-change="handleSelectionChange">
+          <ZmTableColumn
+            type="selection"
+            width="56"
+            :reserve-selection="true"
+            hide-in-column-settings />
+          <ZmTableColumn :label="t('workOrderMaterial.materialName')" prop="name" min-width="220" />
+          <ZmTableColumn :label="t('workOrderMaterial.materialCode')" prop="code" min-width="160" />
+          <ZmTableColumn :label="t('workOrderMaterial.unit')" prop="unit" width="110" />
+          <ZmTableColumn :label="t('route.quantity')" prop="quantity" width="150">
             <template #default="{ row }">
-              <el-checkbox
-                :model-value="selectedRows.some(item => item.id === row.id)"
-                @click.stop="selectRow(row)"
-                class="no-label-radio"
-              />
-            </template>
-          </el-table-column>
-          <el-table-column :label="t('workOrderMaterial.materialName')" align="center" prop="name"
-                           class-name="name-column" min-width="180"/>
-          <el-table-column :label="t('workOrderMaterial.materialCode')" align="center" prop="code" min-width="130"/>
-          <el-table-column :label="t('workOrderMaterial.unit')" align="center" prop="unit" min-width="50"/>
-          <el-table-column :label="t('route.quantity')" align="center" prop="quantity" min-width="80" class-name="quantity-column">
-            <template #default="scope">
-              <el-input
-                type="number"
+              <el-input-number
+                v-model="row.quantity"
                 :controls="false"
-                v-model="scope.row.quantity"
-                @click.stop=""
-                @focus="handleInputFocus(scope.row)"
-                @blur="(event) => handleQuantityBlur(event, scope.row)"
+                :min="0"
+                :precision="2"
+                size="default"
                 class="quantity-input"
-              />
+                @click.stop
+                @focus="selectIfNeeded(row)"
+                @change="() => syncSelectedQuantity(row)" />
             </template>
-          </el-table-column>
-          <el-table-column
+          </ZmTableColumn>
+          <ZmTableColumn
             :label="t('deviceList.createTime')"
-            align="center"
             prop="createTime"
-            min-width="100"
-            :formatter="dateFormatter"
-          />
-        </el-table>
-      </div>
-      <!-- 分页 -->
-      <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
-    </ContentWrap>
-
+            min-width="170"
+            :formatter="dateFormatter" />
+        </ZmTable>
+
+        <div class="pagination-row">
+          <Pagination
+            :total="total"
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            @pagination="getList" />
+        </div>
+      </section>
+    </div>
   </Dialog>
 </template>
 
 <script setup lang="ts">
-import { DictDataVO } from '@/api/system/dict/dict.data'
+import { ElMessage } from 'element-plus'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { dateFormatter } from '@/utils/formatTime'
 import * as MaterialApi from '@/api/pms/material'
-import {defaultProps, handleTree} from "@/utils/tree";
-import * as MaterialGroupApi from '@/api/pms/materialgroup'
-import { shallowRef, nextTick } from 'vue'
 
-defineOptions({
-  name: 'MaterialList'
-})
-const { t } = useI18n() // 国际化
-const materialGroupList = ref<Tree[]>([]) // 物料组树形结构
+defineOptions({ name: 'MaterialList' })
 
+type MaterialRow = Omit<MaterialApi.MaterialVO, 'quantity'> & {
+  quantity?: number
+}
+
+const { t } = useI18n()
+const { ZmTable, ZmTableColumn } = useTableComponents<MaterialRow>()
 const emit = defineEmits<{
   (e: 'choose', value: MaterialApi.MaterialVO[]): void
   (e: 'close'): void
 }>()
 
-const dialogVisible = ref(false) // 弹窗的是否展示
-const loading = ref(true) // 列表的加载中
-const queryFormRef = ref() // 搜索的表单
-const list = ref<MaterialApi.MaterialVO[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const selectedRows = ref<MaterialApi.MaterialVO[]>([]); // 多选数据(存储所有选中行的数组)
-const tableRef = ref<InstanceType<typeof ElTable>>();
-
-const deviceCategoryName = ref('') // 存储设备分类名称
-const bomNodeName = ref('') // 存储BOM节点名称
+const dialogVisible = ref(false)
+const loading = ref(false)
+const queryFormRef = ref()
+const tableRef = ref()
+const list = ref<MaterialRow[]>([])
+const total = ref(0)
+const selectedRows = ref<MaterialRow[]>([])
+const syncingSelection = ref(false)
+const deviceCategoryName = ref('')
+const bomNodeName = ref('')
 
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  materialGroupId: '',
   code: '',
-  name: '',
-  model: '',
-  unit: ''
+  name: ''
 })
 
-const selectedRow = ref(null);
+const selectedIdSet = computed(() => new Set(selectedRows.value.map((item) => item.id)))
 
-// 更新选中物料数量的方法
-const updateSelectedRowQuantity = (row: MaterialApi.MaterialVO) => {
-  const selectedItem = selectedRows.value.find(item => item.id === row.id);
-  if (selectedItem) {
-    selectedItem.quantity = row.quantity;
-  }
-};
+const getSelectedRow = (row: MaterialRow) => {
+  return selectedRows.value.find((item) => item.id === row.id)
+}
 
-const adjustColumnWidths = () => {
-  nextTick(() => {
-    if (tableRef.value) {
-      tableRef.value.doLayout();
-    }
-  });
-};
-
-// 处理单选逻辑
-const selectRow = (row) => {
-  const index = selectedRows.value.findIndex(item => item.id === row.id);
-  if (index > -1) {
-    selectedRows.value.splice(index, 1); // 取消选中
-    row.quantity = undefined;
-  } else {
-    selectedRows.value.push({ ...row }); // 选中
-  }
-};
-
-// 点击整行选中
-const handleRowClick = (row) => {
-  selectRow(row);
-};
-const open = async (row: any) => {
-  // 设置传递过来的设备分类名称和BOM节点名称
-  deviceCategoryName.value = row.deviceCategoryName || ''
-  bomNodeName.value = row.name || ''
+const normalizeQuantity = (quantity?: number) => {
+  const value = Number(quantity)
+  if (!Number.isFinite(value) || value <= 0) return undefined
+  return Number(value.toFixed(2))
+}
 
-  queryParams.name = '';
-  queryParams.code = '';
-  dialogVisible.value = true
+const applySelectedQuantities = () => {
+  list.value.forEach((row) => {
+    const selected = getSelectedRow(row)
+    if (selected) {
+      row.quantity = selected.quantity
+    }
+  })
+}
 
-  await getList();
+const syncCurrentPageSelection = async () => {
+  await nextTick()
+  const elTableRef = tableRef.value?.elTableRef
+  if (!elTableRef) return
+
+  syncingSelection.value = true
+  list.value.forEach((row) => {
+    elTableRef.toggleRowSelection(row, selectedIdSet.value.has(row.id))
+  })
+  await nextTick()
+  syncingSelection.value = false
 }
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
 const getList = async () => {
   loading.value = true
   try {
     const data = await MaterialApi.getMaterialPage(queryParams)
-    list.value = data.list
-    total.value = data.total
-
-    // 同步selectedRows中的数量到当前页数据
-    list.value.forEach(row => {
-      const selectedItem = selectedRows.value.find(item => item.id === row.id);
-      if (selectedItem && selectedItem.quantity !== undefined) {
-        row.quantity = selectedItem.quantity;
-      }
-    });
-
-    // 数据加载后自动调整列宽
-    adjustColumnWidths();
+    list.value = data.list || []
+    total.value = data.total || 0
+    applySelectedQuantities()
+    await syncCurrentPageSelection()
   } finally {
     loading.value = false
   }
 }
 
-// 关闭时清空选择
-const handleClose = () => {
-  tableRef.value?.clearSelection();
+const open = async (row: { deviceCategoryName?: string; name?: string }) => {
+  deviceCategoryName.value = row.deviceCategoryName || ''
+  bomNodeName.value = row.name || ''
+  queryParams.pageNo = 1
+  queryParams.name = ''
+  queryParams.code = ''
   selectedRows.value = []
-  emit('close')
-};
-
-// 移除标签方法
-const removeTag = (material: MaterialApi.MaterialVO) => {
-  // 从已选列表中移除
-  const index = selectedRows.value.findIndex(item => item.id === material.id);
-  if (index !== -1) {
-    selectedRows.value.splice(index, 1);
-  }
+  tableRef.value?.elTableRef?.clearSelection()
+  dialogVisible.value = true
+  await getList()
+}
 
-  // 如果当前行在当前页,取消其选中状态
-  const rowInTable = list.value.find(item => item.id === material.id);
-  if (rowInTable) {
-    const rowIndex = list.value.indexOf(rowInTable);
-    // 这里可以添加逻辑强制更新行状态(如果需要)
-    rowInTable.quantity = undefined;
-  }
-};
-
-// 处理 消耗数量 quantity 输入框失焦事件
-const handleQuantityBlur = (event: Event, row: any) => {
-  const inputValue = (event.target as HTMLInputElement).value;
-  let num = parseFloat(inputValue);
-  // 处理无效值
-  if (isNaN(num)) {
-    row.quantity = 0;
-    return;
-  }
-  // 处理小于等于0的情况
-  if (num <= 0) {
-    row.quantity = 0;
-    return;
-  }
-  // 保留两位小数
-  row.quantity = parseFloat(num.toFixed(2));
-  // 实时更新selectedRows
-  updateSelectedRowQuantity(row);
-};
-
-// 处理输入框焦点事件(自动选中当前行)
-const handleInputFocus = (row: MaterialApi.MaterialVO) => {
-  const getRowUniqueKey = (row) => {
-    return `${row.code}_${row.name}`
-  }
-  const currentKey = getRowUniqueKey(row)
-  // 如果未选中则添加到选中列表
-  const exists = selectedRows.value.some(item => getRowUniqueKey(item) === currentKey)
-  if (!exists) {
-    selectedRows.value.push(row)
+defineExpose({ open })
+
+const selectIfNeeded = (row: MaterialRow) => {
+  if (!selectedIdSet.value.has(row.id)) {
+    tableRef.value?.elTableRef?.toggleRowSelection(row, true)
   }
 }
 
-// 计算列宽
-const flexColumnWidth = (prop: string, label: string, data: any[]) => {
-  if (!data || !data.length) return 'auto';
-
-  const contentWidths = data.map(item => {
-    const value = prop && item[prop] ? String(item[prop]) : '';
-    return getTextWidth(value);
-  });
-
-  const labelWidth = getTextWidth(label);
-  const maxContentWidth = Math.max(...contentWidths);
-  const maxWidth = Math.max(labelWidth, maxContentWidth);
-
-  return Math.min(Math.max(maxWidth + 10, 120), 600) + 'px'; // 10px内边距,120px最小宽度
-};
-
-// 获取文本宽度
-const getTextWidth = (text: string) => {
-  if (!text) return 0;
-
-  const span = document.createElement('span');
-  span.style.visibility = 'hidden';
-  span.style.position = 'absolute';
-  span.style.whiteSpace = 'nowrap';
-  span.style.font = '14px Microsoft YaHei'; // 匹配表格字体
-  span.textContent = text;
-  document.body.appendChild(span);
-
-  const width = span.offsetWidth;
-  document.body.removeChild(span);
-  return width;
-};
-
-// 生成行唯一标识
-const rowKey = (row: any) => {
-  return `${row.id}`; // 确保行更新时重新渲染
-};
-
-// 确认选择
-const handleConfirm = () => {
-  // 同步当前页所有选中行的最新数量
-  list.value.forEach(row => {
-    if (selectedRows.value.some(item => item.id === row.id)) {
-      updateSelectedRowQuantity(row);
+const syncSelectedQuantity = (row: MaterialRow) => {
+  const selected = getSelectedRow(row)
+  if (!selected) return
+  const quantity = normalizeQuantity(row.quantity)
+  selected.quantity = quantity
+  row.quantity = quantity
+}
+
+const handleSelectionChange = (selection: MaterialRow[]) => {
+  if (syncingSelection.value) return
+
+  const currentPageIds = new Set(list.value.map((row) => row.id))
+  const currentSelectionIds = new Set(selection.map((row) => row.id))
+  const previousOtherPageRows = selectedRows.value.filter((row) => !currentPageIds.has(row.id))
+  const currentPageRows = selection.map((row) => {
+    const quantity = normalizeQuantity(row.quantity) || 1
+    row.quantity = quantity
+    return { ...row, quantity }
+  })
+
+  selectedRows.value = [...previousOtherPageRows, ...currentPageRows]
+  list.value.forEach((row) => {
+    if (!currentSelectionIds.has(row.id)) {
+      row.quantity = undefined
     }
-  });
+  })
+}
+
+const handleRowClick = (row: MaterialRow) => {
+  tableRef.value?.elTableRef?.toggleRowSelection(row)
+}
 
-  if (selectedRows.value.length === 0) {
-    ElMessage.warning('请至少选择一个物料')
-    return
-  }
-  emit('choose', selectedRows.value.map(row => ({
-    ...row,
-    // 确保返回必要字段
-    code: row.code,
-    name: row.name,
-    unit: row.unit,
-    quantity: row.quantity,
-  })))
-  dialogVisible.value = false;
-  handleClose()
-};
-
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
-const choose = (row: DictDataVO) => {
-  emit('choose', row)
-  dialogVisible.value = false
-}
-/** 重置按钮操作 */
+
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   handleQuery()
 }
-/** 初始化 **/
-onMounted(async () => {
-  materialGroupList.value = handleTree(await MaterialGroupApi.getSimpleMaterialGroupList())
-  await getList()
-})
 
-// 清除事件监听
-onUnmounted(() => {
-  window.removeEventListener('resize', adjustColumnWidths);
-});
+const handleConfirm = () => {
+  if (!selectedRows.value.length) {
+    ElMessage.warning('请至少选择一个物料')
+    return
+  }
 
-</script>
-<style lang="scss" scoped>
-.no-label-radio .el-radio__label {
-  display: none;
-}
-.no-label-radio .el-radio__inner {
-  margin-right: 0;
+  const invalidMaterial = selectedRows.value.find((item) => !normalizeQuantity(item.quantity))
+  if (invalidMaterial) {
+    ElMessage.warning('请填写大于0的物料数量')
+    return
+  }
+
+  emit(
+    'choose',
+    selectedRows.value.map((row) => ({
+      ...row,
+      quantity: normalizeQuantity(row.quantity)
+    })) as MaterialApi.MaterialVO[]
+  )
+  dialogVisible.value = false
 }
 
-.table-content-wrap {
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  min-height: 300px; // 确保有最小高度
+const handleClose = () => {
+  selectedRows.value = []
+  list.value = []
+  emit('close')
 }
+</script>
 
-.flex-table-container {
-  flex: 1;
+<style lang="scss" scoped>
+.material-picker {
   display: flex;
   flex-direction: column;
-  overflow: hidden;
-
-  // 确保表格填满容器
-  .full-width-table {
-    width: 100%;
-    flex: 1;
-
-    // 让表格头与内容同步宽度
-    :deep(.el-table__header) {
-      width: 100% !important;
-    }
+  gap: 16px;
+}
 
-    :deep(.el-table__body) {
-      width: 100% !important;
-    }
-  }
+.context-panel,
+.filter-panel,
+.table-panel {
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
 }
 
-// 名称列:优先分配剩余空间
-:deep(.name-column) {
-  flex-grow: 1 !important;
-  min-width: 180px;
+.context-panel {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+  padding: 14px 16px;
+  background: var(--el-fill-color-lighter);
+}
 
-  .cell {
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    max-width: 100%;
-  }
+.context-item {
+  min-width: 0;
 }
 
-/* 表格容器样式 */
-.table-container {
-  width: 100%;
-  overflow-x: auto;
+.context-label {
+  margin-right: 10px;
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
 }
 
-/* 自定义淡绿色按钮 */
-:deep(.custom-green-button) {
-  background-color: #e1f3d8;
-  border-color: #e1f3d8;
-  color: #67c23a;
+.context-value {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
 }
 
-/* 悬停效果 */
-:deep(.custom-green-button:hover) {
-  background-color: #d1e8c0;
-  border-color: #d1e8c0;
-  color: #5daf34;
+.filter-panel {
+  padding: 16px 16px 0;
+  background: var(--el-bg-color);
 }
 
-/* 点击效果 */
-:deep(.custom-green-button:active) {
-  background-color: #c2dca8;
-  border-color: #c2dca8;
+.filter-form {
+  display: flex;
+  align-items: flex-start;
+  flex-wrap: wrap;
+  gap: 0 12px;
 }
 
-// 标签样式
-.close-icon {
-  cursor: pointer;
-  color: #999;
-  transition: color 0.2s;
+.filter-input {
+  width: 200px;
+}
 
-  &:hover {
-    color: #ff4d4f;
-  }
+.filter-actions {
+  margin-left: auto;
 }
 
-.el-tag {
-  position: relative;
-  padding-right: 25px;
-  transition: all 0.3s;
+.table-panel {
+  padding: 16px 16px 0;
+  background: var(--el-bg-color);
+}
 
-  &:hover {
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-  }
+.material-table {
+  width: 100%;
 }
 
-/* 调整输入框宽度 */
-:deep(.el-input) {
-  width: 95% !important; /* 保留5%空间避免溢出 */
+.quantity-input {
+  width: 96px;
 }
 
-/* 确保输入框获得焦点时保持可见 */
-:deep(.el-input__inner:focus) {
-  border-color: #409eff !important;
-  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2) !important;
+.quantity-input :deep(.el-input__inner) {
+  text-align: center;
 }
 
-/* 输入框获得焦点时提升层级 */
-.quantity-input:deep(.el-input__inner) {
-  position: relative;
-  z-index: 2;
+.pagination-row {
+  display: flex;
+  justify-content: flex-end;
+  padding: 12px 0 0;
 }
 
+@media (width <= 768px) {
+  .context-panel {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .filter-actions {
+    width: 100%;
+    margin-left: 0;
+  }
+}
 </style>

+ 1 - 12
src/views/pms/bom/MaterialListDrawer.vue

@@ -135,7 +135,7 @@
   </el-drawer>
 </template>
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { ref } from 'vue'
 import { ElMessage, type FormRules } from 'element-plus'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { CommonBomMaterialApi, type CommonBomMaterialVO } from '@/api/pms/commonbommaterial'
@@ -209,16 +209,6 @@ const props = defineProps({
   }
 })
 
-// 监听bom树节点ID变化
-watch(
-  () => props.nodeId,
-  async (newVal) => {
-    if (newVal) {
-      await loadMaterials(newVal)
-    }
-  }
-)
-
 // 加载指定bom节点下的物料数据
 const loadMaterials = async (nodeId?: number, resetPage = true) => {
   if (!nodeId) return
@@ -317,7 +307,6 @@ const closeDrawer = () => {
 // 关闭抽屉
 const handleClose = () => {
   emit('update:modelValue', false)
-  emit('refresh') // 添加刷新事件
   materials.value = []
 }