liuhairui 1 тиждень тому
батько
коміт
02ca7e681a

+ 1 - 0
package.json

@@ -44,6 +44,7 @@
         "vue-echarts": "^6.6.8",
         "vue-plugin-hiprint": "^0.0.56",
         "vue-router": "^4.2.4",
+        "vxe-table": "^4.6.25",
         "xlsx": "^0.18.5"
     },
     "devDependencies": {

+ 264 - 27
src/view/SalaryManage/ProcessProduction.vue

@@ -1,7 +1,7 @@
 <template>
-  <div class="process-production-page" :class="{ 'in-dialog': inDialog }">
+  <div class="process-production-page" :class="{ 'in-dialog': inDialog, 'embedded-in-tab': embeddedInTab }">
     <div class="process-production-inner">
-      <el-form class="process-production-search" inline @keyup.enter="handleSearch">
+      <el-form v-if="!embeddedInTab" class="process-production-search" inline @keyup.enter="handleSearch">
           <el-input
             v-model="searchForm.workorder"
             placeholder="请输入订单编号"
@@ -22,15 +22,25 @@
         :closable="false"
       />
 
-      <el-descriptions class="order-summary-desc" :column="3" border>
-        <el-descriptions-item label="订单编号">
-          {{ orderSummary.订单编号 }}
-        </el-descriptions-item>
-        <el-descriptions-item label="生产款号">
-          {{ orderSummary.生产款号 }}
-        </el-descriptions-item>
-        <el-descriptions-item label="款式">
-          {{ orderSummary.款式 }}
+      <!-- 订单摘要:新增字段只在此处数组里加一行,key 与 WorkOrderList 返回字段一致 -->
+      <el-descriptions
+        v-if="!embeddedInTab"
+        class="order-summary-desc"
+        :column="5"
+        border
+      >
+        <el-descriptions-item
+          v-for="field in [
+            { label: '客户编号', key: '客户编号' },
+            { label: '订单编号', key: '订单编号' },
+            { label: '生产款号', key: '生产款号' },
+            { label: '款式', key: '款式' },
+            { label: '订单数量', key: '订单数量' },
+          ]"
+          :key="field.key"
+          :label="field.label"
+        >
+          {{ orderSummary[field.key] }}
         </el-descriptions-item>
       </el-descriptions>
 
@@ -52,7 +62,31 @@
         >
           <el-table-column label="工序编号" prop="工序号" width="100" align="center"  />
           <el-table-column label="工序名称" prop="工序名称" min-width="280" align="left"  show-overflow-tooltip />
-          <el-table-column label="员工姓名" prop="员工姓名" width="100" align="left" />
+          <el-table-column label="员工姓名" prop="员工姓名" width="120" align="left">
+            <template #header>
+              <div class="process-production-col-header">
+                <span>员工姓名</span>
+                <el-popover trigger="click" placement="bottom-start" :width="180" :teleported="true" :z-index="10050" append-to-body popper-class="gy-detail-filter-popper">
+                  <template #reference>
+                    <span class="gy-detail-filter-btn" :class="{ 'is-active': isStaffNameFilterActive }" @click.stop>
+                      <svg viewBox="0 0 1024 1024" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M880.1 154H143.9c-24.5 0-39.9 26.7-27.6 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48z"/></svg>
+                    </span>
+                  </template>
+                  <div class="gy-detail-filter-panel">
+                    <div class="gy-detail-filter-options">
+                      <el-checkbox-group v-model="staffNameFilter">
+                        <el-checkbox v-for="item in staffNameFilterOptions" :key="item.value" :label="item.value">{{ item.text }}</el-checkbox>
+                      </el-checkbox-group>
+                    </div>
+                    <div class="gy-detail-filter-actions">
+                      <span class="gy-detail-filter-link" @click.stop="clearStaffNameFilter">取消全选</span>
+                      <span class="gy-detail-filter-link" @click.stop="selectAllStaffNameFilter">全选</span>
+                    </div>
+                  </div>
+                </el-popover>
+              </div>
+            </template>
+          </el-table-column>
           <el-table-column label="完工数量" prop="数量" width="90" align="center" />
           <el-table-column v-if="!hideAmountColumns" label="金额" prop="金额" width="90" align="center" />
           <el-table-column v-if="!hideAmountColumns" label="工时" prop="工时" width="90" align="center" />
@@ -75,7 +109,7 @@
 <script setup>
 import { computed, reactive, ref, watch } from 'vue'
 import { ElMessage } from 'element-plus'
-import { checkProcessProduction } from '@/api/mes/job'
+import { checkProcessProduction, WorkOrderList } from '@/api/mes/job'
 
 defineOptions({ name: 'ProcessProduction' })
 
@@ -86,6 +120,8 @@ const props = defineProps({
   inDialog: { type: Boolean, default: false },
   /** 为 true 时不显示金额、工时、工资列(如样衣批核页) */
   hideAmountColumns: { type: Boolean, default: false },
+  /** 嵌入工分报工页 Tab:隐藏搜索栏与订单摘要,固定表格高度 */
+  embeddedInTab: { type: Boolean, default: false },
 })
 
 /** 主页面表格高度:改此值即可 */
@@ -123,8 +159,12 @@ const tableSummaryMethod = ({ columns, data }) => {
   })
 }
 
+/** 嵌入 Tab 时与工分报工记录表同高 */
+const EMBEDDED_TAB_TABLE_HEIGHT = '52vh'
+
 /** 弹窗按行数自适应高度,避免数据少时底部大块空白;主页面固定高度 */
 const tableHeight = computed(() => {
+  if (props.embeddedInTab) return EMBEDDED_TAB_TABLE_HEIGHT
   if (!props.inDialog) return MAIN_TABLE_HEIGHT
   const rows = displayTableData.value.length
   const maxH = Math.max(200, window.innerHeight - DIALOG_TABLE_OFFSET)
@@ -139,7 +179,9 @@ const searched = ref(false)
 const pageHint = ref('')
 
 const emptyDescription = computed(() => {
-  if (!searched.value) return '请输入订单编号后查询'
+  if (!searched.value) {
+    return props.embeddedInTab ? '请先查询订单' : '请输入订单编号后查询'
+  }
   return pageHint.value || '暂无数据'
 })
 
@@ -147,13 +189,13 @@ const searchForm = reactive({
   workorder: '',
 })
 
-const orderSummary = reactive({
-  订单编号: '',
-  生产款号: '',
-  款式: '',
-})
+const orderSummary = reactive({})
+
+const isScalarSummaryValue = (val) =>
+  val == null || (typeof val !== 'object' && typeof val !== 'function')
 
 const processListRaw = ref([])
+const staffNameFilter = ref([])
 
 const flattenProcessList = (processList = []) => {
   const rows = []
@@ -194,7 +236,87 @@ const flattenProcessList = (processList = []) => {
   return rows
 }
 
-const displayTableData = computed(() => flattenProcessList(processListRaw.value || []))
+const flatTableRows = computed(() => flattenProcessList(processListRaw.value || []))
+
+const staffNameFilterOptions = computed(() => {
+  const names = [...new Set(
+    flatTableRows.value
+      .map(row => String(row.员工姓名 || '').trim())
+      .filter(Boolean)
+  )]
+  return names.map(name => ({ text: name, value: name }))
+})
+
+const isStaffNameFilterActive = computed(() => {
+  const allValues = staffNameFilterOptions.value.map(item => item.value)
+  const selected = staffNameFilter.value
+  if (!allValues.length) return false
+  return selected.length < allValues.length
+})
+
+const selectAllStaffNameFilter = () => {
+  staffNameFilter.value = staffNameFilterOptions.value.map(item => item.value)
+}
+
+const clearStaffNameFilter = () => {
+  staffNameFilter.value = []
+}
+
+const syncStaffNameFilterAfterDataChange = () => {
+  const allValues = staffNameFilterOptions.value.map(item => item.value)
+  const selected = staffNameFilter.value
+  if (!selected.length) {
+    staffNameFilter.value = [...allValues]
+    return
+  }
+  const wasAllSelected = allValues.length > 0 && allValues.every(v => selected.includes(v))
+  if (wasAllSelected) {
+    staffNameFilter.value = [...allValues]
+    return
+  }
+  staffNameFilter.value = selected.filter(v => allValues.includes(v))
+}
+
+const applyStaffNameFilter = (rows) => {
+  const options = staffNameFilterOptions.value
+  const selected = staffNameFilter.value
+  if (!selected.length) return []
+  if (!options.length || selected.length >= options.length) return rows
+  const set = new Set(selected)
+  return rows.filter((row) => {
+    const name = String(row.员工姓名 || '').trim()
+    if (!name) return true
+    return set.has(name)
+  })
+}
+
+const recomputeProcessRowSpan = (rows) => {
+  const result = []
+  let i = 0
+  while (i < rows.length) {
+    const processNo = rows[i].工序号
+    const processName = rows[i].工序名称
+    let j = i
+    while (j < rows.length && rows[j].工序号 === processNo && rows[j].工序名称 === processName) {
+      j++
+    }
+    const group = rows.slice(i, j)
+    group.forEach((row, idx) => {
+      result.push({ ...row, _processRowSpan: idx === 0 ? group.length : 0 })
+    })
+    i = j
+  }
+  return result
+}
+
+const displayTableData = computed(() => {
+  const filtered = applyStaffNameFilter(flatTableRows.value)
+  return recomputeProcessRowSpan(filtered)
+})
+
+watch(flatTableRows, () => {
+  syncStaffNameFilterAfterDataChange()
+})
 
 const tableSpanMethod = ({ row, column }) => {
   if (column.property !== '工序号' && column.property !== '工序名称') {
@@ -207,6 +329,40 @@ const tableSpanMethod = ({ row, column }) => {
   return { rowspan: 0, colspan: 0 }
 }
 
+const resetOrderSummary = (workorder = '') => {
+  Object.keys(orderSummary).forEach((key) => {
+    orderSummary[key] = ''
+  })
+  orderSummary.订单编号 = workorder
+}
+
+const applyOrderSummaryFromWorkOrder = (data, workorder) => {
+  Object.keys(orderSummary).forEach((key) => {
+    orderSummary[key] = ''
+  })
+  for (const [key, val] of Object.entries(data || {})) {
+    if (isScalarSummaryValue(val)) {
+      orderSummary[key] = val ?? ''
+    }
+  }
+  orderSummary.订单编号 = orderSummary.订单编号 || workorder
+}
+
+const loadOrderSummaryFromWorkOrderList = async (workorder) => {
+  resetOrderSummary(workorder)
+  try {
+    const res = await WorkOrderList({ search: workorder, page: 1, limit: 1 })
+    const list = res?.data?.data
+    if (res?.code === 0 && Array.isArray(list) && list.length) {
+      applyOrderSummaryFromWorkOrder(list[0], workorder)
+      return true
+    }
+  } catch (error) {
+    console.error(error)
+  }
+  return false
+}
+
 const loadByWorkorder = async (workorder) => {
   const w = String(workorder || '').trim()
   if (!w) return
@@ -225,20 +381,18 @@ const handleSearch = async () => {
   searched.value = true
   pageHint.value = ''
   try {
-    const res = await checkProcessProduction({ workorder })
+    const [, res] = await Promise.all([
+      loadOrderSummaryFromWorkOrderList(workorder),
+      checkProcessProduction({ workorder }),
+    ])
+
     if (res?.code !== 0) {
       processListRaw.value = []
-      orderSummary.订单编号 = workorder
-      orderSummary.生产款号 = ''
-      orderSummary.款式 = ''
       pageHint.value = res?.msg || '未找到报工数据'
       return
     }
 
     const data = res.data || {}
-    orderSummary.订单编号 = data.订单编号 || workorder
-    orderSummary.生产款号 = data.生产款号 || ''
-    orderSummary.款式 = data.款式 || ''
     processListRaw.value = data.工序列表 || []
     if (!processListRaw.value.length) {
       pageHint.value = '未找到报工数据'
@@ -248,8 +402,10 @@ const handleSearch = async () => {
     pageHint.value = ''
     ElMessage.error('查询失败,请稍后重试')
     processListRaw.value = []
+    resetOrderSummary(workorder)
   } finally {
     loading.value = false
+    searchForm.workorder = ''
   }
 }
 
@@ -286,6 +442,10 @@ defineExpose({ loadByWorkorder })
   max-width: 100%;
 }
 
+.process-production-page.embedded-in-tab .process-production-inner {
+  padding: 0;
+}
+
 .process-production-search {
   margin-bottom: 8px;
 }
@@ -328,4 +488,81 @@ defineExpose({ loadByWorkorder })
   color: #303133;
   background: #fafafa;
 }
+
+.process-production-col-header {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+}
+
+.gy-detail-filter-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 22px;
+  height: 22px;
+  cursor: pointer;
+  color: #909399;
+  border-radius: 2px;
+}
+
+.gy-detail-filter-btn:hover,
+.gy-detail-filter-btn.is-active {
+  color: var(--el-color-primary);
+  background: #ecf5ff;
+}
+
+.gy-detail-filter-options {
+  max-height: 220px;
+  overflow-x: hidden;
+  overflow-y: auto;
+  margin-bottom: 4px;
+}
+
+.gy-detail-filter-panel :deep(.el-checkbox) {
+  display: flex;
+  margin-right: 0;
+  margin-bottom: 6px;
+}
+
+.gy-detail-filter-actions {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 4px;
+  padding-top: 8px;
+  border-top: 1px solid #ebeef5;
+}
+
+.gy-detail-filter-link {
+  color: var(--el-color-primary);
+  font-size: 12px;
+  line-height: 1;
+  cursor: pointer;
+  user-select: none;
+  padding: 4px 2px;
+}
+
+.gy-detail-filter-link:hover {
+  opacity: 0.85;
+}
+</style>
+
+<style>
+.gy-detail-filter-popper {
+  z-index: 10050 !important;
+  pointer-events: auto;
+  padding: 8px 10px !important;
+}
+
+.gy-detail-filter-popper .gy-detail-filter-panel {
+  pointer-events: auto;
+}
+
+.gy-detail-filter-popper .gy-detail-filter-panel .el-checkbox {
+  display: flex;
+  margin-right: 0;
+  margin-bottom: 6px;
+}
 </style>

+ 130 - 39
src/view/SalaryManage/SalaryList.vue

@@ -16,13 +16,14 @@
                 style="width: 200px"
               />
               <el-button type="primary" icon="search" class="bt" @click="applyStaffSearch">查询</el-button>
+              <el-button type="primary" icon="search" class="bt" @click="openProcessProductionDialog">工序产量核查</el-button>
             </el-form-item>
           </el-form>
         </div>
       </layout-header>
 
-      <layout>
-        <layout-sider :resize-directions="['right']" :width="190" style="margin: 0px">
+      <layout class="salary-list-body">
+        <layout-sider :resize-directions="['right']" :width="190" class="salary-list-sider">
           <div v-loading="menuLoading" class="JKWTree-tree staff-tree-panel salary-tree-panel-fill">
             <h3 class="salary-tree-h3">报工月份 / 工序</h3>
             <div class="salary-tree-scroll">
@@ -155,6 +156,35 @@
       </layout>
     </layout>
 
+    <el-dialog
+      v-model="processProductionVisible"
+      width="100%"
+      style="height: 100%; margin: 0"
+      top="2vh"
+      :close-on-click-modal="true"
+      :show-close="false"
+      destroy-on-close
+      class="process-production-dialog"
+    >
+      <template #header>
+        <div class="process-production-dialog-header">
+          <span class="process-production-dialog-title">工序产量核查</span>
+          <el-button
+            type="danger"
+            size="small"
+            class="process-production-dialog-close"
+            @click="processProductionVisible = false"
+          >
+            关闭
+          </el-button>
+        </div>
+      </template>
+      <ProcessProduction
+        :key="processProductionDialogKey"
+        in-dialog
+      />
+    </el-dialog>
+
     <el-dialog
       v-model="detailVisible"
       :title="detailDialogTitle"
@@ -177,31 +207,20 @@
           show-summary
           :summary-method="getDetailSummaries"
         >
-        <el-table-column label="订单编号" prop="订单编号" min-width="110" show-overflow-tooltip />
-        <el-table-column label="日期" prop="日期" width="110" align="center" />
-        <el-table-column
-          v-if="detailShowPartCols"
-          label="部件名称"
-          prop="部件名称"
-          min-width="100"
-          show-overflow-tooltip
-        />
-        <el-table-column
-          v-if="detailShowPartCols"
-          label="部件编号"
-          prop="部件编号"
-          width="90"
-          align="center"
-        />
-        <el-table-column label="工资" prop="工资" width="90" align="right" />
-        <el-table-column label="数量" prop="数量" width="80" align="right" />
-        <el-table-column label="生产工时" prop="生产工时" width="100" align="right" />
-        <el-table-column label="生产分数" prop="生产分数" width="100" align="right" />
-        <el-table-column label="设备名称" prop="设备名称" width="90" show-overflow-tooltip />
-        <el-table-column label="工序名称" prop="工序名称" min-width="140" show-overflow-tooltip />
-        <el-table-column label="标准工时" prop="标准工时" width="90" align="right" />
-        <el-table-column label="标准分数" prop="标准分数" width="90" align="right" />
-        <el-table-column label="系数" prop="系数" width="72" align="right" />
+        <el-table-column label="订单编号" prop="订单编号" width="110" show-overflow-tooltip />
+        <el-table-column v-if="detailShowPartCols" label="部件名称" prop="部件名称" width="90" show-overflow-tooltip />
+        <el-table-column v-if="detailShowPartCols" label="部件编号" prop="部件编号" width="90" align="center" />
+        <el-table-column label="工序名称" prop="工序名称" min-width="280" show-overflow-tooltip />
+        <el-table-column label="小组" prop="设备编号" width="110" align="center" />
+        <el-table-column label="上报数量" prop="数量" width="80" align="center" />
+        <el-table-column label="工资" prop="工资" width="90" align="center" />
+        <el-table-column label="生产工时(秒)" prop="生产工时" width="120" align="center" />
+        <el-table-column label="生产分数" prop="生产分数" width="100" align="center" />
+        <el-table-column label="标准工时" prop="标准工时" width="90" align="center" />
+        <el-table-column label="标准分数" prop="标准分数" width="90" align="center" />
+        <el-table-column label="难度系数" prop="系数" width="100" align="center" />
+        <el-table-column label="上报日期" prop="日期" width="110" align="center" />
+
           <template #empty>
             <el-empty
               v-if="!detailLoading"
@@ -224,9 +243,18 @@ import {
   GetStaffSalaryList,
   GetStaffSalaryDetail,
 } from '@/api/mes/job'
+import ProcessProduction from '@/view/SalaryManage/ProcessProduction.vue'
 
 defineOptions({ name: 'SalaryList' })
 
+const processProductionVisible = ref(false)
+const processProductionDialogKey = ref(0)
+
+const openProcessProductionDialog = () => {
+  processProductionDialogKey.value += 1
+  processProductionVisible.value = true
+}
+
 /** 与报工/产品侧一致:工序展示顺序 */
 const BIG_PROCESS_ORDER = ['裁剪', '车缝', '手工', '大烫', '总检']
 
@@ -304,13 +332,16 @@ const detailVisible = ref(false)
 const detailLoading = ref(false)
 const detailRows = ref([])
 const detailTitle = ref('工资明细')
-const detailContext = ref({ staff_no: '', date: '', big_process: '' })
+const detailContext = ref({ staff_no: '', staff_name: '', date: '', big_process: '' })
 
 const detailShowPartCols = computed(() => detailContext.value.big_process === '车缝')
 
 const detailDialogTitle = computed(() => {
-  const { staff_no, date } = detailContext.value
-  if (staff_no && date) return `工资明细 · ${staff_no} · ${date}`
+  const { staff_no, staff_name, date } = detailContext.value
+  if (staff_no && date) {
+    const name = String(staff_name ?? '').trim()
+    return name ? `${staff_no} · ${name} · ${date}` : `${staff_no} · ${date}`
+  }
   return detailTitle.value
 })
 
@@ -533,6 +564,7 @@ function openSalaryDetailForRowDay(row, dayNum) {
   if (t - pivotDblOpenLock < 400) return
   pivotDblOpenLock = t
   const no = String(row['员工编号'] ?? '').trim()
+  const name = String(row['员工姓名'] ?? '').trim()
   if (!no || no === '—') {
     ElMessage.warning('缺少员工编号')
     return
@@ -542,6 +574,7 @@ function openSalaryDetailForRowDay(row, dayNum) {
   const dateStr = `${month}-${String(dayNum).padStart(2, '0')}`
   detailContext.value = {
     staff_no: no,
+    staff_name: name,
     date: dateStr,
     big_process: String(selectedBigProcess.value || '').trim(),
   }
@@ -609,7 +642,7 @@ async function loadDetailRows() {
 
 function onDetailClosed() {
   detailRows.value = []
-  detailContext.value = { staff_no: '', date: '', big_process: '' }
+  detailContext.value = { staff_no: '', staff_name: '', date: '', big_process: '' }
 }
 
 //总数量合计
@@ -719,21 +752,34 @@ onMounted(() => {
   flex: 1;
   min-height: 0;
   overflow: hidden;
+  align-items: stretch !important;
+}
+.salary-list-body {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
 }
 .salary-list-page :deep(.arco-layout-sider) {
-  background: var(--el-fill-color-light) !important;
+  background: #fff !important;
   border-right: 1px solid var(--el-border-color-lighter);
-  align-self: stretch;
-  min-height: 0;
+  align-self: stretch !important;
+  height: auto !important;
+  min-height: 100%;
+  margin: 0 !important;
+  display: flex;
+  flex-direction: column;
 }
-.salary-list-page :deep(.arco-layout-sider-children) {
-  padding-top: 0 !important;
+.salary-list-page :deep(.salary-list-sider.arco-layout-sider) {
   padding-bottom: 0 !important;
+}
+.salary-list-page :deep(.arco-layout-sider-children) {
+  padding: 0 !important;
   height: 100% !important;
-  min-height: 0;
+  min-height: 100%;
   display: flex;
   flex-direction: column;
   overflow: hidden;
+  box-sizing: border-box;
 }
 .salary-list-page :deep(.arco-layout-content) {
   padding-top: 0 !important;
@@ -764,12 +810,24 @@ onMounted(() => {
   flex-direction: column;
   overflow: hidden;
 }
-/* 与人力页 renyuan 一致,但本页要铺满侧栏:取消 420 上限,树在 .salary-tree-scroll 内滚 */
+/* 与人力页 renyuan 一致,侧栏树撑满高度,避免下方留白 */
 .staff-tree-panel {
   min-height: 260px;
   max-height: 420px;
   overflow: auto;
 }
+.salary-list-page .staff-tree-panel.salary-tree-panel-fill {
+  min-height: 100% !important;
+  max-height: none !important;
+  height: 100%;
+  flex: 1 1 auto;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-sizing: border-box;
+  padding: 10px 6px 10px 10px;
+  background: #fff;
+}
 .salary-tree-panel-fill {
   min-height: 0 !important;
   max-height: none !important;
@@ -779,7 +837,6 @@ onMounted(() => {
   flex-direction: column;
   overflow: hidden;
   box-sizing: border-box;
-  padding: 8px 6px 8px 8px;
 }
 .salary-tree-h3 {
   margin: 0 0 6px;
@@ -879,4 +936,38 @@ onMounted(() => {
 :deep(.salary-detail-table .el-table__body-wrapper) {
   -webkit-overflow-scrolling: touch;
 }
+
+.process-production-dialog .el-dialog__header {
+  display: block;
+  width: 100%;
+  margin-right: 0;
+  padding: 16px 20px 12px;
+  box-sizing: border-box;
+}
+
+.process-production-dialog .el-dialog__body {
+  padding: 0;
+  background: #fff;
+}
+
+.process-production-dialog-header {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.process-production-dialog-title {
+  flex: 1;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.process-production-dialog-close {
+  flex-shrink: 0;
+  margin-left: auto;
+  min-width: 56px;
+  font-weight: 500;
+}
 </style>

Різницю між файлами не показано, бо вона завелика
+ 649 - 272
src/view/performance/WorkScoreReporting/gongfenbaogong.vue


+ 7 - 7
src/view/performance/caiqiebaogong.vue

@@ -131,7 +131,7 @@
 		      <el-main>
 				<!-- 表格数据 -->
 				  <el-tabs v-model="activeName">
-					  <el-tab-pane label="报工历史记录"   @click="showTable('报工历史记录')" name="first">
+					  <el-tab-pane label="报工记录"   @click="showTable('报工历史记录')" name="first">
 						<el-table ref="multipleTable" style="width: 100%;height: 52vh" tooltip-effect="dark"
 								  :row-style="{ height: '50px' }" :header-cell-style="{ padding: '5px', fontSize: '20px' }"
 								  :cell-style="{ padding: '10px', fontSize: '20px' }" :header-row-style="{ height: '20px' }"
@@ -141,9 +141,9 @@
 								  @row-click="tableRowClick" :show-overflow-tooltip="true"
 								  @selection-change="selectionChange">
 								  <el-table-column type="selection" width="60" />
-								  <el-table-column align="left" label="报工时间"	prop="sys_rq"  		width="150"/>
-								  <el-table-column align="left" label="子订单编号" prop="子订单编号"  	width="130"/>
-								  <el-table-column align="left" label="生产款号" 	prop="款号"  	width="120"/>
+								  <el-table-column align="left" label="报工时间"	prop="sys_rq"  		width="155"/>
+								  <el-table-column align="left" label="子订单编号" prop="子订单编号"  	width="135"/>
+								  <el-table-column align="left" label="生产款号" 	prop="款号"  	width="260"/>
 								  <el-table-column align="left" label="款式" 		prop="款式"  		width="100"/>
 								  <el-table-column align="left" label="组别" 		prop="sczl_jtbh"  		width="80"/>
 								  <el-table-column align="left" label="尺码" 		prop="尺码"  		width="70"/>
@@ -162,9 +162,9 @@
 								  highlight-current-row="true" @row-dblclick="updateCompanyFunc"
 								  @row-click="tableRowClick" :show-overflow-tooltip="true"
 								  @selection-change="handleSelectionChange">
-								  <el-table-column  align="left" label="订单子编号" prop="子订单编号"  width="130"/>
-								  <el-table-column  align="left" label="款号" prop="款号" width="140" />
-								  <el-table-column  align="left" label="色系名称" prop="颜色" width="100" />
+								  <el-table-column  align="left" label="订单子编号" prop="子订单编号"  width="135"/>
+								  <el-table-column  align="left" label="生产款号" prop="款号" width="260" />
+								  <el-table-column  align="left" label="色系名称" prop="颜色" width="130" />
 								  <el-table-column v-for="item in sizeDatas" :key="item" align="left" :label="item" :prop="item" width="60" :cell-style="cellStyle">
 									<template v-slot="scope">
 										<div style="width: 80px;" @click="handleSizeClick(scope.$index, item, scope.row)">

+ 215 - 29
src/view/yunyin/shengchanguanli/Orderprogress.vue

@@ -1,13 +1,23 @@
 <template>
     <!-- 搜索区域 -->
     <!-- <div class="search-container"> -->
-    <div class="">
+    <div class="order-progress-toolbar">
         <el-form-item style="margin-bottom: 0px;">
-          <el-input v-model="searchInfo" placeholder="搜索订单编号/生产款号/款式" @keyup.enter="handleSearch" style="width: 300px;height: 40px;"></el-input>
-          <el-button type="primary" class="search" @click="handleSearch" style="height: 40px;">
-            <el-icon><Search /></el-icon>
-            查询
-          </el-button>
+          <el-input
+            v-model="searchInfo"
+            placeholder="搜索订单编号/生产款号/款式"
+            @keyup.enter="handleSearch"
+            class="order-progress-search-input"
+          />
+          <el-button type="primary" icon="search" @click="handleSearch">查询</el-button>
+          <el-button type="primary"  icon="edit" :disabled="!selectedOrderNo" @click="cp_gdprintonClick" >订单打印</el-button>
+          <el-button
+            type="primary"
+            icon="search"
+            :loading="processProductionChecking"
+            :disabled="!selectedOrderNo"
+            @click="openProcessProductionDialog"
+          >工序产量核查</el-button>
         </el-form-item>
     </div>
     
@@ -137,6 +147,62 @@
           </div>
     </div>
   </div>
+
+  <!-- 工序产量核查 -->
+  <el-dialog
+    v-model="processProductionVisible"
+    width="100%"
+    style="height: 100%; margin: 0"
+    top="2vh"
+    :close-on-click-modal="true"
+    :show-close="false"
+    destroy-on-close
+    class="process-production-dialog"
+  >
+    <template #header>
+      <div class="process-production-dialog-header">
+        <span class="process-production-dialog-title">工序产量核查</span>
+        <el-button
+          type="danger"
+          size="small"
+          class="process-production-dialog-close"
+          @click="processProductionVisible = false"
+        >
+          关闭
+        </el-button>
+      </div>
+    </template>
+    <ProcessProduction
+      :key="processProductionDialogKey"
+      :initial-workorder="processProductionWorkorder"
+      in-dialog
+      hide-amount-columns
+    />
+  </el-dialog>
+
+  <!-- 订单打印 -->
+  <el-dialog
+    v-model="orderPrintVisible"
+    title="选择打印格式"
+    width="30%"
+    top="10%"
+    destroy-on-close
+    @close="orderPrintVisible = false"
+  >
+    <el-form>
+      <el-form-item label="打印方向" label-width="100px">
+        <el-radio-group v-model="printDirection">
+          <el-radio label="纵向">纵向</el-radio>
+          <el-radio label="横向">横向</el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="orderPrintVisible = false">关闭</el-button>
+      <el-button type="primary" @click="confirmPrintDirection">确定打印</el-button>
+    </template>
+  </el-dialog>
+  <PrintPage ref="printPageRef" />
 </template>
 <script>
 //点击按钮显示下方表格
@@ -159,10 +225,12 @@ export default {
 <script setup>
 // 全量引入格式化工具 请按需保留
 import { Layout, LayoutSider, LayoutContent } from '@arco-design/web-vue';
-import {ref, reactive, onMounted, onUnmounted} from 'vue'
+import {ref, reactive, computed, nextTick, onUnmounted} from 'vue'
 import {GetOrderprogress} from '@/api/mes/job'
 import {ElMessage} from "element-plus";
-import { Search } from '@element-plus/icons-vue';
+import ProcessProduction from '@/view/SalaryManage/ProcessProduction.vue'
+import PrintPage from './components/print.vue'
+import { ensureProcessProductionData } from '@/utils/processProductionDialog'
 import * as XLSX from 'xlsx';
 import FileSaver from 'file-saver';
 // import { get } from 'scriptjs';
@@ -186,27 +254,37 @@ const currentDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
 const currentDates = `${year}-${month}-${day}`;
 
 
-// 获取环境变量服务器IP地址
-const basePath = import.meta.env.VITE_BASE_PATH ?? 'http://192.168.28.22';
-const uploadsPort = import.meta.env.VITE_UPLOADS_PORT ?? '8082';
-console.log("basePath", basePath);
-console.log("uploadsPort", uploadsPort);
-// 动态生成路径
-let base = 'http://192.168.28.22:8082';
-console.log("最终获取地址:", base);
+// 上传图片服务根地址(兼容 basePath 带尾斜杠、已含端口等情况)
+const getUploadsBaseUrl = () => {
+  let basePath = String(import.meta.env.VITE_BASE_PATH || 'http://192.168.28.22').trim()
+  const uploadsPort = String(import.meta.env.VITE_UPLOADS_PORT || '8082').replace(/^:/, '')
+
+  if (!basePath || basePath === 'undefined') {
+    basePath = 'http://192.168.28.22'
+  }
+
+  basePath = basePath.replace(/\/+$/, '')
+
+  // 已含端口则直接使用,避免重复拼接 :8082
+  if (/^https?:\/\/[^/]+:\d+/.test(basePath)) {
+    return basePath
+  }
+
+  return `${basePath}:${uploadsPort}`
+}
 
 // 格式化图片URL,拼接服务器地址
 const formatImageUrl = (path) => {
-  if (!path) return '';
-  // 如果图片路径已经包含完整URL,则直接返回
-  if (path.startsWith('http://') || path.startsWith('https://')) {
-    return path;
+  if (!path) return ''
+  const trimmed = String(path).trim()
+  if (/^https?:\/\//i.test(trimmed)) {
+    return trimmed
   }
-  // 否则拼接base路径
-  const imagePath = path.replace(/^public\//, ''); // 移除可能的public前缀
- return `${basePath}:${uploadsPort}${imagePath}`;
-  // return `${base}/${imagePath}`;
-};
+  const base = getUploadsBaseUrl()
+  const imagePath = trimmed.replace(/^public\//, '')
+  const rel = imagePath.startsWith('/') ? imagePath : `/${imagePath}`
+  return `${base}${rel}`
+}
 
 // =========== 生产进度相关 =========== 
 const searchInfo = ref('');
@@ -215,6 +293,53 @@ const pageSize = ref(30); // 默认每页显示X条
 const total = ref(0);
 const restableData = ref([]);
 const selectedOrder = ref(null); // 当前选中的订单数据
+const multipleTable = ref(null)
+const selectedOrderNo = computed(() => String(selectedOrder.value?.订单编号 || '').trim())
+const printPageRef = ref(null)
+
+// 工序产量核查
+const processProductionVisible = ref(false)
+const processProductionWorkorder = ref('')
+const processProductionDialogKey = ref(0)
+const processProductionChecking = ref(false)
+
+const openProcessProductionDialog = async () => {
+  if (!selectedOrderNo.value) {
+    ElMessage.warning('请先在表格中点击选择订单')
+    return
+  }
+  processProductionChecking.value = true
+  try {
+    const result = await ensureProcessProductionData(selectedOrderNo.value)
+    if (!result.ok) return
+    processProductionWorkorder.value = result.workorder
+    processProductionDialogKey.value += 1
+    processProductionVisible.value = true
+  } finally {
+    processProductionChecking.value = false
+  }
+}
+
+// 订单打印
+const orderPrintVisible = ref(false)
+const printDirection = ref('纵向')
+
+const cp_gdprintonClick = () => {
+  if (!selectedOrderNo.value) {
+    ElMessage.warning('请先在表格中点击选择订单')
+    return
+  }
+  orderPrintVisible.value = true
+}
+
+const confirmPrintDirection = () => {
+  if (!selectedOrderNo.value) {
+    ElMessage.warning('请先在表格中点击选择订单')
+    return
+  }
+  printPageRef.value?.open(selectedOrderNo.value, printDirection.value)
+  orderPrintVisible.value = false
+}
           
 // 生产工序配置
 const processSteps = ref([
@@ -280,6 +405,16 @@ const isCurrentStep = (processName) => {
   return false;
 };
 
+const syncTableCurrentRow = () => {
+  nextTick(() => {
+    if (selectedOrder.value) {
+      multipleTable.value?.setCurrentRow?.(selectedOrder.value)
+    } else {
+      multipleTable.value?.setCurrentRow?.()
+    }
+  })
+}
+
 const getStaffList = async () => {
   try {
     const params = {
@@ -295,12 +430,12 @@ const getStaffList = async () => {
       restableData.value = response.data || [];
       total.value = response.count || 0;
       console.log('获取数据成功,共', total.value, '条记录');
-      // 搜索后,默认选中第一条数据
       if (restableData.value.length > 0) {
         selectedOrder.value = restableData.value[0];
       } else {
         selectedOrder.value = null;
       }
+      syncTableCurrentRow()
     } else {
       console.warn('接口返回非成功状态:', response);
       ElMessage.warning('获取数据时出现问题,请稍后重试');
@@ -314,7 +449,6 @@ const getStaffList = async () => {
 // 初始化数据加载
 const getReceiptTabs = async () => {
   await getStaffList();
-  // 页面加载时,默认选中第一条数据
   if (restableData.value.length > 0) {
     selectedOrder.value = restableData.value[0];
   }
@@ -340,9 +474,8 @@ const handleCurrentChange = () => {
 
 // 表格行点击事件
 const tableRowClick = (row) => {
-  console.log('选中行:', row);
-  // 点击表格行时,更新选中的订单数据
   selectedOrder.value = row;
+  multipleTable.value?.setCurrentRow?.(row)
 };
 
 // 表格单元格样式
@@ -409,6 +542,23 @@ onUnmounted(() => {
   background-color: #fff;
   border-bottom: 1px solid #e6e8eb;
 }
+.order-progress-toolbar :deep(.el-form-item__content) {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+.order-progress-search-input {
+  width: 300px;
+}
+.order-progress-toolbar :deep(.order-progress-search-input .el-input__wrapper) {
+  height: 32px;
+}
+.order-progress-toolbar :deep(.el-button) {
+  height: 32px;
+  padding: 0 15px;
+  margin: 0;
+}
 /* 内容区域 */
 .content-container {
   overflow: auto;
@@ -778,3 +928,39 @@ onUnmounted(() => {
   font-size: 14px; 
 }
 </style>
+
+<style>
+.process-production-dialog .el-dialog__header {
+  display: block;
+  width: 100%;
+  margin-right: 0;
+  padding: 16px 20px 12px;
+  box-sizing: border-box;
+}
+
+.process-production-dialog .el-dialog__body {
+  padding: 0;
+  background: #fff;
+}
+
+.process-production-dialog-header {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.process-production-dialog-title {
+  flex: 1;
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.process-production-dialog-close {
+  flex-shrink: 0;
+  margin-left: auto;
+  min-width: 56px;
+  font-weight: 500;
+}
+</style>

+ 357 - 34
src/view/yunyin/shengchanguanli/dahuobaobiao.vue

@@ -1,6 +1,6 @@
 <template>
-  <div>
-    <layout>
+  <div ref="dahuobaobiaoPageRef" class="dahuobaobiao-page">
+    <layout class="dahuobaobiao-root">
       <layout-header>
         <div class="top-action-bar action-buttons">
           <el-input
@@ -27,33 +27,38 @@
 
         <!-- 右侧表格区域 -->
         <layout-content class="dahuobaobiao-content">
-          <el-main class="dahuobaobiao-main">
-            <div class="gva-table-box">
-              <!-- 表格数据 -->
-              <el-table ref="multipleTable" style="width: 100%; height: 40vh" tooltip-effect="dark"
+          <div class="dahuobaobiao-main">
+            <div class="dahuobaobiao-panel dahuobaobiao-panel--main">
+              <div ref="mainTableWrapRef" class="dahuobaobiao-table-wrap dahuobaobiao-table-wrap--main">
+              <el-table
+                ref="multipleTable"
+                :height="mainTableHeight"
+                style="width: 100%"
+                tooltip-effect="dark"
                         :row-style="{ height: '25px' }" :header-cell-style="{ padding: '0px' }"
                         :cell-style="{ padding: '0px' }" :header-row-style="{ height: '20px' }"
                         :data="tableData" border row-key="ID"
                         size="small"
                         highlight-current-row="true"
+                        scrollbar-always-on
                         @row-click="tableRowClick" @selection-change="handleSelectionChange" :show-overflow-tooltip="true">
                 <el-table-column type="selection" width="40" fixed="left" />
 
-                <el-table-column sortable align="center" label="款式" prop="款式" width="120" />
+                <el-table-column sortable align="left" label="款式" prop="款式" width="130" />
                 <el-table-column sortable align="center" label="客人编号" prop="客人编号" width="100" />
                 <el-table-column sortable align="center" label="下单日期" prop="下单日期" width="100" />
                 <el-table-column sortable align="center" label="货期" prop="货期" width="100" />
-                <el-table-column sortable align="center" label="款号" prop="款号" width="150" />
+                <el-table-column sortable align="center" label="款号" prop="款号" width="170" />
                 <el-table-column align="center" label="计划生产小组" width="140">
                   <template #default="{ row }">
                     <el-autocomplete
                       v-model="row['计划生产小组']"
                       :fetch-suggestions="queryPlanGroupSuggestions"
                       clearable
-                      placeholder="请选择或输入"
                       style="width: 100%"
                       value-key="value"
                       @select="(item) => savePlanGroupRow(row, item?.value ?? item)"
+                      @focus="() => onPlanGroupFocus(row)"
                       @blur="() => savePlanGroupRowOnBlur(row)"
                       @clear="() => savePlanGroupRow(row, '')"
                     />
@@ -95,6 +100,7 @@
                 <el-table-column sortable align="center" label="后道完成日期" prop="后道完成日期" width="140" />
                 <el-table-column sortable align="center" label="备注" prop="备注" width="150" />
               </el-table>
+              </div>
 
               <!-- 分页 -->
               <div class="gva-pagination">
@@ -111,16 +117,24 @@
             </div>
 
             <!-- 下方BOM表格 -->
-            <div class="gva-table-box dahuobaobiao-detail-box">
+            <div class="dahuobaobiao-panel dahuobaobiao-panel--detail">
               <h3 class="dahuobaobiao-detail-title">订单开工详情</h3>
-              <el-table ref="bomTable" style="width: 100%; height: 30vh" tooltip-effect="dark"
+              <div ref="detailTableWrapRef" class="dahuobaobiao-table-wrap dahuobaobiao-table-wrap--detail">
+              <el-table
+                ref="bomTable"
+                :height="detailTableHeight"
+                style="width: 100%"
+                tooltip-effect="dark"
                         :row-style="{ height: '25px' }" :header-cell-style="{ padding: '0px' }"
                         :cell-style="{ padding: '0px' }" :header-row-style="{ height: '20px' }"
                         :data="bomData" border
                         size="small"
+                        scrollbar-always-on
                         :show-overflow-tooltip="true">
                 <el-table-column prop="物料名称" label="物料名称" width="200" />
-                <el-table-column prop="核批日期" label="核批日期" width="110" />
+                <el-table-column prop="核批日期" label="核批日期" width="110">
+                  <template #default="{ row }">{{ stripDateTime(row.核批日期) }}</template>
+                </el-table-column>
                 <el-table-column prop="核批人" label="核批人" width="130" />
                 <el-table-column prop="计划入库时间" label="计划入库时间" width="140">
                   <template #default="{ row }">
@@ -133,12 +147,15 @@
                     />
                   </template>
                 </el-table-column>
-                <el-table-column prop="实际入库时间" label="实际入库时间" width="130" />
+                <el-table-column prop="实际入库时间" label="实际入库时间" width="130">
+                  <template #default="{ row }">{{ stripDateTime(row.实际入库时间) }}</template>
+                </el-table-column>
                 <el-table-column prop="入库数量" label="入库数量" width="100" />
                 <el-table-column prop="计划入库操作人" label="计划入库操作人" width="130" />
               </el-table>
+              </div>
             </div>
-          </el-main>
+          </div>
         </layout-content>
       </layout>
     </layout>
@@ -176,7 +193,7 @@
   </div>
 </template>
   <script setup>
-  import { ref, reactive, onMounted, computed } from 'vue'
+  import { ref, reactive, onMounted, onUnmounted, onActivated, onDeactivated, nextTick, computed } from 'vue'
   import { Layout, LayoutSider, LayoutContent, LayoutHeader } from '@arco-design/web-vue'
   import {
     orderBomList, FabricEdit, Approval, AccessoriesInboundTime,
@@ -194,6 +211,84 @@
   const userStore = useUserStore()
   const _username = ref('')
   _username.value = userStore.userInfo.userName + '/' + userStore.userInfo.nickName
+
+  const dahuobaobiaoPageRef = ref(null)
+  const mainTableWrapRef = ref(null)
+  const detailTableWrapRef = ref(null)
+  const mainTableHeight = ref(320)
+  const detailTableHeight = ref(220)
+  let tableLayoutResizeObserver = null
+  let mainElResizeObserver = null
+  let layoutMainEl = null
+  let layoutMainOverflowSaved = ''
+
+  const syncDahuobaobiaoPageHeight = () => {
+    const root = dahuobaobiaoPageRef.value
+    if (!root) return
+    const main = root.closest('.el-main')
+    if (!main) return
+    const mainBr = main.getBoundingClientRect()
+    const rootBr = root.getBoundingClientRect()
+    let h = Math.floor(mainBr.bottom - rootBr.top - 8)
+    if (!Number.isFinite(h) || h < 120) {
+      const cs = getComputedStyle(main)
+      const padY = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0)
+      h = Math.max(120, main.clientHeight - padY - 8)
+    }
+    root.style.height = `${h}px`
+    root.style.maxHeight = `${h}px`
+    root.style.minHeight = '0'
+    root.style.overflow = 'hidden'
+    updateTableHeights()
+  }
+
+  const onPageLayoutResize = () => {
+    syncDahuobaobiaoPageHeight()
+  }
+
+  const lockLayoutMainOverflow = () => {
+    const main = dahuobaobiaoPageRef.value?.closest?.('.el-main')
+    if (!main) return
+    if (layoutMainEl !== main) {
+      layoutMainOverflowSaved = main.style.overflow || ''
+    }
+    layoutMainEl = main
+    main.style.overflow = 'hidden'
+  }
+
+  const restoreLayoutMainOverflow = () => {
+    if (layoutMainEl) {
+      layoutMainEl.style.overflow = layoutMainOverflowSaved
+    }
+    layoutMainEl = null
+  }
+
+  const updateTableHeights = () => {
+    nextTick(() => {
+      requestAnimationFrame(() => {
+        const mainH = mainTableWrapRef.value?.clientHeight || 0
+        const detailH = detailTableWrapRef.value?.clientHeight || 0
+        if (mainH > 120) {
+          mainTableHeight.value = Math.floor(mainH)
+        }
+        if (detailH > 100) {
+          detailTableHeight.value = Math.floor(detailH)
+        }
+      })
+    })
+  }
+
+  const attachTableLayoutResizeObserver = () => {
+    tableLayoutResizeObserver?.disconnect()
+    if (typeof ResizeObserver === 'undefined') return
+    tableLayoutResizeObserver = new ResizeObserver(updateTableHeights)
+    if (mainTableWrapRef.value) {
+      tableLayoutResizeObserver.observe(mainTableWrapRef.value)
+    }
+    if (detailTableWrapRef.value) {
+      tableLayoutResizeObserver.observe(detailTableWrapRef.value)
+    }
+  }
   
   
   // 左侧树形数据
@@ -206,7 +301,7 @@
   const tableData = reactive([])
   const page = ref(1)
   const total = ref(0)
-  const pageSize = ref(30)
+  const pageSize = ref(100)
   /** 列表查询方式:month 按月份 / search 按关键字 */
   const listQueryMode = ref('month')
 
@@ -230,8 +325,8 @@
     const sortedData = [...(list || [])].sort((a, b) => {
       return (a['货期'] || '').localeCompare(b['货期'] || '')
     })
-    const formattedData = sortedData.map((item, index) =>
-      initRowPlanGroupPath({
+    const formattedData = sortedData.map((item, index) => {
+      const row = initRowPlanGroupPath({
         ID: rowOffset + index + 1,
         ...item,
         '下单日期': formatDate(item['下单日期']),
@@ -242,10 +337,15 @@
         '车位完成日期': formatDate(item['车位完成日期']),
         '后道完成日期': formatDate(item['后道完成日期']),
       })
-    )
+      row.辅料计划入库时间 = stripDateTime(row.辅料计划入库时间)
+      row.核批日期 = stripDateTime(row.核批日期)
+      initRowEditableSavedState(row)
+      return row
+    })
     tableData.splice(0, tableData.length, ...formattedData)
     total.value = totalCount
     bomData.value = []
+    updateTableHeights()
   }
 
   /** 请求列表(服务端分页:page + limit) */
@@ -383,6 +483,41 @@
   }
 
   const planGroupSavedMap = new WeakMap()
+  const planGroupFocusMap = new WeakMap()
+  const accessoryDateSavedMap = new WeakMap()
+  const approvalDateSavedMap = new WeakMap()
+  const planDateSavedMap = new WeakMap()
+
+  const stripDateTime = (val) => {
+    if (val == null || String(val).trim() === '') return ''
+    const s = String(val).trim()
+    const matched = s.match(/^(\d{4}-\d{2}-\d{2})/)
+    if (matched) return matched[1]
+    if (s.includes(' ')) return s.split(' ')[0]
+    return s
+  }
+
+  const normalizeDateVal = (val) => {
+    if (val == null || String(val).trim() === '') return ''
+    return stripDateTime(val)
+  }
+
+  const initRowEditableSavedState = (row) => {
+    planGroupSavedMap.set(row, String(row['计划生产小组'] ?? '').trim())
+    accessoryDateSavedMap.set(row, normalizeDateVal(row.辅料计划入库时间))
+    approvalDateSavedMap.set(row, normalizeDateVal(row.核批日期))
+  }
+
+  const initBomRowSavedState = (row) => {
+    row.核批日期 = stripDateTime(row.核批日期)
+    row.计划入库时间 = stripDateTime(row.计划入库时间)
+    row.实际入库时间 = stripDateTime(row.实际入库时间)
+    planDateSavedMap.set(row, normalizeDateVal(row.计划入库时间))
+  }
+
+  const onPlanGroupFocus = (row) => {
+    planGroupFocusMap.set(row, String(row['计划生产小组'] ?? '').trim())
+  }
 
   const queryPlanGroupSuggestions = (queryString, cb) => {
     const q = String(queryString ?? '').trim().toLowerCase()
@@ -402,9 +537,14 @@
     if (ok) planGroupSavedMap.set(row, name)
   }
 
-  /** 自定义输入后未点选下拉项,失焦时也会保存 */
+  /** 自定义输入后未点选下拉项,失焦时也会保存(仅值有变化时) */
   const savePlanGroupRowOnBlur = async (row) => {
-    await savePlanGroupRow(row, row['计划生产小组'])
+    if (!planGroupFocusMap.has(row)) return
+    const before = planGroupFocusMap.get(row)
+    planGroupFocusMap.delete(row)
+    const current = String(row['计划生产小组'] ?? '').trim()
+    if (before === current) return
+    await savePlanGroupRow(row, current)
   }
 
   const loadPlanGroupOptions = async () => {
@@ -530,7 +670,9 @@
       // 获取BOM数据
       const orderBomListdata = await orderBomList({ order: row.订单编号 });
       if (orderBomListdata.code === 0) {
-        bomData.value = orderBomListdata.data;
+        bomData.value = orderBomListdata.data
+        bomData.value.forEach((item) => initBomRowSavedState(item))
+        updateTableHeights()
       } else {
         ElMessage.error('获取BOM数据失败');
       }
@@ -707,10 +849,48 @@
   onMounted(() => {
     getDateList();
     loadPlanGroupOptions();
+    nextTick(() => {
+      lockLayoutMainOverflow()
+      syncDahuobaobiaoPageHeight()
+      requestAnimationFrame(() => {
+        syncDahuobaobiaoPageHeight()
+        attachTableLayoutResizeObserver()
+      })
+    })
+    window.addEventListener('resize', onPageLayoutResize)
+    nextTick(() => {
+      const mainEl = dahuobaobiaoPageRef.value?.closest?.('.el-main')
+      if (mainEl) {
+        mainElResizeObserver = new ResizeObserver(onPageLayoutResize)
+        mainElResizeObserver.observe(mainEl)
+      }
+    })
+  })
+
+  onUnmounted(() => {
+    window.removeEventListener('resize', onPageLayoutResize)
+    tableLayoutResizeObserver?.disconnect()
+    mainElResizeObserver?.disconnect()
+    restoreLayoutMainOverflow()
+  })
+
+  onActivated(() => {
+    nextTick(() => {
+      lockLayoutMainOverflow()
+      syncDahuobaobiaoPageHeight()
+    })
+  })
+
+  onDeactivated(() => {
+    restoreLayoutMainOverflow()
   })
 
   // 计划入库时间编辑相关
   const handlePlanDateChange = async (row, newDate) => {
+    const next = normalizeDateVal(newDate ?? row.计划入库时间)
+    const saved = normalizeDateVal(planDateSavedMap.get(row))
+    if (saved === next) return
+
     try {
       const formattedData = [{
         BOM_工单编号: selectedOrder.value.订单编号,
@@ -736,6 +916,7 @@
 
       const add_FabricEditdata = await FabricEdit(formattedData);
       if (add_FabricEditdata.code === 0) {
+        planDateSavedMap.set(row, next)
         ElMessage.success('修改成功');
       } else {
         ElMessage.error('修改失败');
@@ -748,6 +929,10 @@
 
   // 样衣核批日期编辑相关
   const handleApprovalDateChange = async (row, newDate) => {
+    const next = normalizeDateVal(newDate ?? row.核批日期)
+    const saved = normalizeDateVal(approvalDateSavedMap.get(row))
+    if (saved === next) return
+
     try {
       if (!row.Uniqid) {
         ElMessage.error('缺少Uniqid,无法提交');
@@ -764,6 +949,7 @@
       });
       
       if (response.code === 0) {
+        approvalDateSavedMap.set(row, next)
         ElMessage.success('样衣核批提交成功');
       } else {
         ElMessage.error('样衣核批提交失败');
@@ -776,6 +962,10 @@
 
   /** 辅料计划入库时间:AccessoriesInboundTime 传 Uniqid + rq(日期) + sys_id(不传 计划生产小组) */
   const handleAccessoryDateChange = async (row, newDate) => {
+    const next = normalizeDateVal(newDate ?? row.辅料计划入库时间)
+    const saved = normalizeDateVal(accessoryDateSavedMap.get(row))
+    if (saved === next) return
+
     try {
       if (!row.Uniqid) {
         ElMessage.error('缺少Uniqid,无法提交');
@@ -792,6 +982,7 @@
       });
 
       if (response.code === 0) {
+        accessoryDateSavedMap.set(row, next)
         ElMessage.success('辅料计划入库时间修改成功');
       } else {
         ElMessage.error('辅料计划入库时间修改失败');
@@ -1001,6 +1192,7 @@
     padding: 10px 20px;
     background: #fff;
     border-bottom: 1px solid #e8e8e8;
+    flex-shrink: 0;
   }
 
   .action-buttons {
@@ -1009,60 +1201,178 @@
     flex-wrap: wrap;
   }
 
-  /* 左右栏底部对齐:侧栏与右侧双表区域同高 */
+  .dahuobaobiao-page {
+    display: flex;
+    flex-direction: column;
+    width: 100%;
+    box-sizing: border-box;
+    overflow: hidden;
+    min-height: 0;
+  }
+
+  .dahuobaobiao-page > :deep(.dahuobaobiao-root),
+  .dahuobaobiao-page > :deep(.arco-layout) {
+    flex: 1;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+
+  .dahuobaobiao-root {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
+  }
+
+  .dahuobaobiao-root :deep(.arco-layout-header) {
+    flex-shrink: 0;
+  }
+
+  /* 左右栏同高,底部对齐 */
+  .dahuobaobiao-root :deep(.dahuobaobiao-body-layout) {
+    flex: 1;
+    min-height: 0;
+    height: 100%;
+    display: flex !important;
+    flex-direction: row !important;
+    align-items: stretch !important;
+    overflow: hidden;
+  }
+
   .dahuobaobiao-body-layout {
+    flex: 1;
+    min-height: 0;
     align-items: stretch !important;
+    overflow: hidden;
+  }
+
+  .dahuobaobiao-page :deep(.arco-layout-has-sider) {
+    flex: 1;
+    min-height: 0;
+    overflow: hidden;
   }
 
   .dahuobaobiao-sider {
     margin-right: 0 !important;
     align-self: stretch !important;
+    height: auto !important;
+    min-height: 100%;
+    display: flex;
+    flex-direction: column;
+    background: #fff;
+    border-right: 1px solid #e8e8e8;
+    box-sizing: border-box;
+  }
+
+  .dahuobaobiao-body-layout :deep(.arco-layout-sider) {
+    display: flex;
+    flex-direction: column;
+    align-self: stretch !important;
+    height: auto !important;
+    min-height: 100%;
+    padding-bottom: 0 !important;
+    background: #fff;
   }
 
   .dahuobaobiao-body-layout :deep(.arco-layout-sider-children) {
+    flex: 1;
     height: 100%;
     display: flex;
     flex-direction: column;
-    padding-bottom: 10px !important;
+    padding: 0 !important;
     box-sizing: border-box;
+    min-height: 0;
   }
 
   .dahuobaobiao-content {
     padding-left: 0 !important;
+    flex: 1;
+    min-width: 0;
+    height: auto !important;
+    min-height: 100%;
+    align-self: stretch !important;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
   }
 
   .dahuobaobiao-body-layout :deep(.arco-layout-content) {
     padding-bottom: 0 !important;
+    flex: 1;
+    min-width: 0;
+    height: auto !important;
+    min-height: 100%;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
   }
 
   .dahuobaobiao-main {
-    padding: 0 10px 10px 0 !important;
+    height: 100%;
+    min-height: 0;
+    padding: 0 10px 0 0 !important;
     box-sizing: border-box;
+    display: flex;
+    flex-direction: column;
+    gap: 0;
+    overflow: hidden;
+  }
+
+  .dahuobaobiao-panel {
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+
+  .dahuobaobiao-panel--main {
+    flex: 3;
+    min-height: 0;
+  }
+
+  .dahuobaobiao-panel--detail {
+    flex: 2;
+    min-height: 160px;
+    margin-top: 0;
+    padding-top: 0;
+    border-top: none;
   }
 
-  .dahuobaobiao-detail-box {
-    margin-top: 6px;
+  .dahuobaobiao-table-wrap {
+    overflow: hidden;
+  }
+
+  .dahuobaobiao-table-wrap--main {
+    flex: 1;
+    min-height: 0;
+  }
+
+  .dahuobaobiao-table-wrap--detail {
+    flex: 1;
+    min-height: 0;
   }
 
   .dahuobaobiao-detail-title {
     font-size: 15px;
     font-weight: 700;
-    margin: 0 0 6px;
+    margin: 0 0 4px;
     padding: 0;
     line-height: 1.4;
+    flex-shrink: 0;
   }
 
-  /* 左侧树状图区域:高度与右侧 40vh+30vh 及分页、标题区一致 */
+  /* 左侧树状图:撑满侧栏高度,与右侧表格底部对齐 */
   .dahuobaobiao-sider .JKWTree-tree {
-    flex: 1;
-    min-height: calc(70vh + 72px);
-    height: calc(70vh + 72px);
-    max-height: calc(70vh + 72px);
+    flex: 1 1 auto;
+    height: 100%;
+    min-height: 100%;
+    max-height: none;
     overflow-x: hidden;
     overflow-y: auto;
     padding: 10px 6px 10px 10px;
     background: #fff;
-    border-right: 1px solid #e8e8e8;
     box-sizing: border-box;
   }
 
@@ -1079,6 +1389,19 @@
 
   .gva-pagination {
     text-align: right;
+    flex-shrink: 0;
+    padding: 0;
+    margin: 0;
+    line-height: 1;
+  }
+
+  .dahuobaobiao-panel--main .gva-pagination {
+    margin-bottom: 0;
+  }
+
+  .dahuobaobiao-panel--main :deep(.el-pagination) {
+    margin-top: 0;
+    padding-top: 0;
   }
 
   /* 表格单元格样式 */

Деякі файли не було показано, через те що забагато файлів було змінено