liuhairui 1 Minggu lalu
induk
melakukan
024e0da43f

+ 1 - 0
src/api/yunyin/product.js

@@ -184,6 +184,7 @@ export const ProcessLibAdd = (data) => {
     })
 }
 
+/** 工艺库列表;可传 page、limit、search(生产工序/关键词等,以后端为准) */
 export const ProcessList = (params) => {
     return service({
         url: '/mes_server/Process_Lib/ProcessList',

+ 85 - 0
src/utils/productProcessExcel.js

@@ -21,6 +21,41 @@ export const PROCESS_EXCEL_EXPORT_HEADER_ORDER = [
 
 export const PROCESS_EXCEL_EXPORT_PAGE_SIZE = 500
 
+/** 与工艺列表拉全量一致:单页条数受后端上限时多页合并 */
+export const PRODUCT_PART_LIST_PAGE_SIZE = 500
+
+/**
+ * ProductPartList 可能两种形态:
+ * - 平铺:list 即部件行(可多条,如 9 条即 9 个部件)
+ * - 嵌套:list 为产品行,部件在 item.parts;工艺在 item.parts[].processes
+ *
+ * 注意:若 first.parts 为 [] 仍满足 Array.isArray,旧逻辑会误走嵌套并对每行 flatMap 成空,只剩「有子 parts」的少数行,出现「接口 9 个、界面 6 个」。
+ */
+function looksLikePartRow(r) {
+  if (!r || typeof r !== 'object') return false
+  return (
+    r.part_sort != null ||
+    r.part_name != null ||
+    r['部件名称'] != null ||
+    r['部件序号'] != null
+  )
+}
+
+export function flattenProductPartListItems(list) {
+  if (!Array.isArray(list) || !list.length) return []
+  // 多条且每条都像部件行 → 平铺,勿按「嵌套产品」再展开
+  if (list.length > 1 && list.every(looksLikePartRow)) {
+    return list
+  }
+  const first = list[0]
+  if (first && typeof first === 'object' && Array.isArray(first.parts) && first.parts.length > 0) {
+    return list.flatMap((row) =>
+      Array.isArray(row?.parts) && row.parts.length ? row.parts : []
+    )
+  }
+  return list
+}
+
 /** 导出文件名中的片段:去掉 Windows 非法字符 */
 export function sanitizeExcelFileNameSegment(s, maxLen = 100) {
   const t = String(s ?? '')
@@ -166,6 +201,56 @@ export async function fetchAllGyRowsForExport(productCode, ProductGyList, pageSi
   return { ok: true, rows: all }
 }
 
+/** @param {Function} ProductPartList API 方法 */
+export async function fetchAllProductPartRows(
+  productCode,
+  ProductPartList,
+  pageSize = PRODUCT_PART_LIST_PAGE_SIZE
+) {
+  const code = String(productCode ?? '').trim()
+  const all = []
+  let page = 1
+  const maxPages = 200
+  const limit = pageSize
+  /** 嵌套结构时 list[0] 带产品信息,供明细等仅展示、不再请求 ProductList */
+  let productMeta = null
+  while (page <= maxPages) {
+    const res = await ProductPartList({
+      product_code: code,
+      page,
+      limit,
+    })
+    if (res?.code !== 0) {
+      return { ok: false, msg: res?.msg || '获取部件列表失败', rows: [], productMeta: null }
+    }
+    const payload = res.data || {}
+    const chunk = Array.isArray(payload.list) ? payload.list : []
+    if (page === 1 && chunk[0] && typeof chunk[0] === 'object') {
+      const r0 = chunk[0]
+      if (r0.product_name != null || r0.product_type != null || r0.product_code != null) {
+        productMeta = {
+          product_code: String(r0.product_code ?? code ?? '').trim() || code,
+          product_name: String(r0.product_name ?? r0.productName ?? '').trim(),
+          product_type: String(r0.product_type ?? r0.productType ?? '').trim(),
+        }
+      }
+    }
+    const flatParts = flattenProductPartListItems(chunk)
+    all.push(...flatParts)
+    const total = payload.count ?? payload.total
+    const isNested = !!(chunk[0] && Array.isArray(chunk[0].parts))
+    if (isNested) {
+      if (chunk.length < limit) break
+      if (total != null && Number.isFinite(Number(total)) && page >= Number(total)) break
+    } else {
+      if (flatParts.length < limit) break
+      if (total != null && Number.isFinite(Number(total)) && all.length >= Number(total)) break
+    }
+    page += 1
+  }
+  return { ok: true, rows: all, productMeta }
+}
+
 export function formatProcessExcelPreviewCell(row, column, cellValue) {
   const prop = column?.property
   if (prop === '标准工时' || prop === '标准工分' || prop === '难度系数') {

+ 804 - 0
src/view/SalaryManage/SalaryList.vue

@@ -0,0 +1,804 @@
+<template>
+  <div class="yunyin-hr-split salary-list-page">
+    <layout>
+      <layout-header class="yunyin-page-header">
+        <div>
+          <el-form
+            class="demo-form-inline"
+            @keyup.enter="applyStaffSearch"
+          >
+            <el-form-item>
+              <el-input
+                v-model="staffSearchInput"
+                placeholder="查询员工编号或员工姓名"
+                clearable
+                class="search"
+                style="width: 200px"
+              />
+              <el-button type="primary" icon="search" class="bt" @click="applyStaffSearch">查询</el-button>
+            </el-form-item>
+          </el-form>
+        </div>
+      </layout-header>
+
+      <layout>
+        <layout-sider :resize-directions="['right']" :width="190" style="margin: 0px">
+          <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">
+              <el-tree
+                ref="menuTreeRef"
+                class="salary-menu-tree"
+                :data="menuTreeData"
+                :props="treeProps"
+                node-key="id"
+                default-expand-all
+                highlight-current
+                :expand-on-click-node="false"
+                @node-click="onMenuNodeClick"
+              >
+                <template #default="{ data }">
+                  <span class="tree-node-label">{{ data.label }}</span>
+                </template>
+              </el-tree>
+            </div>
+            <el-empty
+              v-if="!menuLoading && !menuTreeData.length"
+              class="salary-tree-empty"
+              description="暂无月份数据"
+              :image-size="72"
+            />
+          </div>
+        </layout-sider>
+
+        <layout-content>
+          <el-main class="yunyin-main-block">
+            <div class="gva-table-box">
+              <el-table
+                v-if="menuReady"
+                v-loading="listLoading"
+                :data="pagedPivotRows"
+                border
+                row-key="rowKey"
+                stripe
+                highlight-current-row
+                class="salary-pivot-table"
+                style="width: 100%; height: 72vh"
+                :row-style="{ height: '30px' }"
+                :header-row-style="{ height: '20px' }"
+                :header-cell-style="pivotHeaderCellStyle"
+                :cell-style="pivotCellStyle"
+                :show-overflow-tooltip="true"
+                @cell-dblclick="onPivotDblOpenDetail"
+                @row-dblclick="onPivotDblOpenDetail"
+              >
+                <el-table-column
+                  label="员工编号"
+                  prop="员工编号"
+                  width="118"
+                  align="center"
+                  fixed="left"
+                  show-overflow-tooltip
+                />
+                <el-table-column
+                  label="员工姓名"
+                  prop="员工姓名"
+                  min-width="100"
+                  align="center"
+                  fixed="left"
+                  show-overflow-tooltip
+                />
+                <el-table-column
+                  v-for="d in DAY_INDEXES"
+                  :key="'d' + d"
+                  :prop="'day' + d"
+                  :label="String(d)"
+                  width="76"
+                  align="center"
+                  class-name="salary-day-col"
+                >
+                  <template #default="{ row }">
+                    <span
+                      class="salary-day-cell"
+                      :class="{
+                        'is-outside': d > daysInSelectedMonth,
+                      }"
+                      :title="
+                        d <= daysInSelectedMonth && row['_has' + d]
+                          ? '双击可查看该日工资明细'
+                          : undefined
+                      "
+                      @dblclick="onDayCellDblClick(row, d)"
+                    >{{ row['day' + d] || '' }}</span>
+                  </template>
+                </el-table-column>
+                <template #empty>
+                  <el-empty
+                    v-if="!listLoading"
+                    description="该月份与工序下暂无工资数据"
+                    :image-size="100"
+                  />
+                </template>
+              </el-table>
+              <div v-if="menuReady && !listLoading" class="gva-pagination">
+                <el-pagination
+                  v-model:current-page="pivotPage"
+                  v-model:page-size="pivotPageSize"
+                  :page-sizes="[20, 50, 100, 200]"
+                  :total="pivotRowsFiltered.length"
+                  layout="total, sizes, prev, pager, next, jumper"
+                  background
+                />
+              </div>
+              <div
+                v-else-if="!listLoading"
+                class="salary-main-placeholder"
+              >
+                <el-empty
+                  description="请在左侧选择报工月份与生产工序"
+                  :image-size="120"
+                />
+              </div>
+            </div>
+          </el-main>
+        </layout-content>
+      </layout>
+    </layout>
+
+    <el-dialog
+      v-model="detailVisible"
+      :title="detailDialogTitle"
+      align-center
+      destroy-on-close
+      append-to-body
+      class="salary-detail-dialog"
+      :close-on-click-modal="true"
+      @closed="onDetailClosed"
+    >
+      <div class="salary-detail-table-wrap">
+        <el-table
+          v-loading="detailLoading"
+          :data="detailRows"
+          border
+          size="small"
+          class="salary-detail-table"
+          :height="SALARY_DETAIL_TABLE_FIXED_PX"
+          style="width: 100%"
+        >
+        <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" />
+          <template #empty>
+            <el-empty
+              v-if="!detailLoading"
+              description="暂无明细"
+              :image-size="64"
+            />
+          </template>
+        </el-table>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { Layout, LayoutHeader, LayoutSider, LayoutContent } from '@arco-design/web-vue'
+import { ref, computed, onMounted, nextTick, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import {
+  GetReportingWorkMonth,
+  GetStaffSalaryList,
+  GetStaffSalaryDetail,
+} from '@/api/mes/job'
+
+defineOptions({ name: 'SalaryList' })
+
+/** 与报工/产品侧一致:工序展示顺序 */
+const BIG_PROCESS_ORDER = ['裁剪', '车缝', '手工', '大烫', '总检']
+
+function sortBigProcessList(arr) {
+  if (!Array.isArray(arr)) return []
+  return [...new Set(arr)].sort((a, b) => {
+    const ia = BIG_PROCESS_ORDER.indexOf(a)
+    const ib = BIG_PROCESS_ORDER.indexOf(b)
+    if (ia === -1 && ib === -1) return String(a).localeCompare(String(b), 'zh-CN')
+    if (ia === -1) return 1
+    if (ib === -1) return -1
+    return ia - ib
+  })
+}
+
+const treeProps = { label: 'label', children: 'children' }
+const menuTreeRef = ref(null)
+const menuLoading = ref(false)
+/** 接口 data:{ "2026-04": ["裁剪","车缝"] } */
+const monthProcessMap = ref({})
+const menuTreeData = ref([])
+
+const selectedMonth = ref('')
+const selectedBigProcess = ref('')
+const menuReady = computed(() => !!(selectedMonth.value && selectedBigProcess.value))
+
+const listLoading = ref(false)
+/** 员工为行、1~31 为列的透视表(接口全量,不含搜索) */
+const pivotRowsAll = ref([])
+
+const staffSearchInput = ref('')
+/** 点击「查询」后生效的筛选关键字 */
+const searchKeywordApplied = ref('')
+
+const pivotPage = ref(1)
+const pivotPageSize = ref(50)
+
+/** 始终 1~31 列,与旧版日计件表一致 */
+const DAY_INDEXES = Array.from({ length: 31 }, (_, i) => i + 1)
+
+const pivotRowsFiltered = computed(() => {
+  const rows = pivotRowsAll.value
+  const kw = (searchKeywordApplied.value || '').trim().toLowerCase()
+  if (!kw) return rows
+  return rows.filter((r) => {
+    const no = String(r['员工编号'] ?? '')
+      .toLowerCase()
+    const name = String(r['员工姓名'] ?? '')
+      .toLowerCase()
+    return no.includes(kw) || name.includes(kw)
+  })
+})
+
+const pagedPivotRows = computed(() => {
+  const list = pivotRowsFiltered.value
+  const start = (pivotPage.value - 1) * pivotPageSize.value
+  return list.slice(start, start + pivotPageSize.value)
+})
+
+watch([pivotRowsFiltered, pivotPageSize], ([arr, ps]) => {
+  const n = arr.length
+  const maxPage = n === 0 ? 1 : Math.max(1, Math.ceil(n / ps))
+  if (pivotPage.value > maxPage) pivotPage.value = maxPage
+})
+
+const daysInSelectedMonth = computed(() => {
+  const ym = String(selectedMonth.value || '').trim()
+  if (!/^\d{4}-\d{2}$/.test(ym)) return 31
+  const [y, m] = ym.split('-').map(Number)
+  if (!m || m < 1 || m > 12) return 31
+  return new Date(y, m, 0).getDate()
+})
+
+const detailVisible = ref(false)
+const detailLoading = ref(false)
+const detailRows = ref([])
+const detailTitle = ref('工资明细')
+const detailContext = ref({ staff_no: '', 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}`
+  return detailTitle.value
+})
+
+/** 工资明细表固定高度(px),表体在框内滚动,不随行数撑高整窗 */
+const SALARY_DETAIL_TABLE_FIXED_PX = 500
+
+function parseStaffKey(staffStr) {
+  const s = String(staffStr ?? '').trim()
+  const m = s.match(/^(\d+)\s*[-–—]\s*(.+)$/)
+  if (m) return { staff_no: m[1].trim(), staff_name: m[2].trim() }
+  return { staff_no: '', staff_name: s || '—' }
+}
+
+/** 从日期串解析为「日」,且必须属于 monthYm(YYYY-MM) */
+function parseDayInMonth(dateStr, monthYm) {
+  if (!dateStr || !monthYm) return -1
+  const ds = String(dateStr).trim()
+  const [yy, mm] = String(monthYm).split('-').map(Number)
+  const m = ds.match(/^(\d{4})[-/.年](\d{1,2})[-/.月日]?(\d{1,2})/)
+  if (m) {
+    const y = Number(m[1])
+    const mo = Number(m[2])
+    const day = Number(m[3])
+    if (y === yy && mo === mm && day >= 1 && day <= 31) return day
+  }
+  return -1
+}
+
+/**
+ * 将 GetStaffSalaryList 转为一行一人、day1~day31 为当日工资
+ * 同月同日多条 children 时金额相加
+ */
+function buildPivotRows(list, monthYm) {
+  const dim = (() => {
+    if (!monthYm || !/^\d{4}-\d{2}$/.test(monthYm)) return 31
+    const [y, m] = monthYm.split('-').map(Number)
+    return new Date(y, m, 0).getDate()
+  })()
+  const out = []
+  let i = 0
+  for (const item of list || []) {
+    const p = parseStaffKey(item.staff)
+    const baseNo = String(item['员工编号'] ?? p.staff_no ?? '').trim()
+    const baseName = String(item['员工姓名'] ?? p.staff_name ?? '').trim()
+    const sumByDay = {}
+    for (const c of item.children || []) {
+      const day = parseDayInMonth(c['日期'], monthYm)
+      if (day < 1 || day > dim) continue
+      const w = c['工资']
+      if (w === null || w === undefined || w === '') continue
+      const n = Number(w)
+      if (Number.isNaN(n)) continue
+      sumByDay[day] = (sumByDay[day] || 0) + n
+    }
+    const row = {
+      rowKey: `pv-${i++}-${baseNo}`,
+      员工编号: baseNo || '—',
+      员工姓名: baseName || '—',
+    }
+    for (let d = 1; d <= 31; d++) {
+      const keyH = '_has' + d
+      if (d > dim) {
+        row['day' + d] = ''
+        row[keyH] = false
+        continue
+      }
+      const v = sumByDay[d]
+      if (v != null && !Number.isNaN(Number(v))) {
+        row['day' + d] = Number(v).toFixed(2)
+        row[keyH] = true
+      } else {
+        row['day' + d] = ''
+        row[keyH] = false
+      }
+    }
+    out.push(row)
+  }
+  return out
+}
+
+function buildMenuTree(data) {
+  if (!data || typeof data !== 'object') return []
+  const keys = Object.keys(data).sort((a, b) => String(b).localeCompare(String(a), 'zh-CN'))
+  const out = []
+  for (const month of keys) {
+    const procs = sortBigProcessList(data[month])
+    out.push({
+      id: `m-${month}`,
+      label: month,
+      isProcess: false,
+      children: procs.map((p) => ({
+        id: `${month}||${p}`,
+        label: p,
+        isProcess: true,
+        month,
+        big_process: p,
+      })),
+    })
+  }
+  return out
+}
+
+async function loadLeftMenu() {
+  menuLoading.value = true
+  try {
+    const res = await GetReportingWorkMonth({})
+    if (res?.code === 0) {
+      monthProcessMap.value = res.data && typeof res.data === 'object' ? res.data : {}
+      menuTreeData.value = buildMenuTree(monthProcessMap.value)
+    } else {
+      monthProcessMap.value = {}
+      menuTreeData.value = []
+    }
+  } catch (e) {
+    console.error(e)
+    ElMessage.error('加载月份菜单失败')
+    monthProcessMap.value = {}
+    menuTreeData.value = []
+  } finally {
+    menuLoading.value = false
+  }
+}
+
+function onMenuNodeClick(data) {
+  if (!data?.isProcess || !data.month || !data.big_process) return
+  selectedMonth.value = data.month
+  selectedBigProcess.value = data.big_process
+  staffSearchInput.value = ''
+  searchKeywordApplied.value = ''
+  pivotPage.value = 1
+  nextTick(() => {
+    try {
+      menuTreeRef.value?.setCurrentKey?.(data.id)
+    } catch {
+      // ignore
+    }
+  })
+  loadStaffList()
+}
+
+async function loadStaffList() {
+  const month = String(selectedMonth.value || '').trim()
+  const big_process = String(selectedBigProcess.value || '').trim()
+  if (!month || !big_process) {
+    pivotRowsAll.value = []
+    return
+  }
+  listLoading.value = true
+  try {
+    const res = await GetStaffSalaryList({ month, big_process })
+    if (res?.code === 0) {
+      const raw = res.data
+      const list = Array.isArray(raw) ? raw : raw?.list ?? []
+      pivotRowsAll.value = buildPivotRows(list, month)
+      pivotPage.value = 1
+    } else {
+      pivotRowsAll.value = []
+    }
+  } catch (e) {
+    console.error(e)
+    pivotRowsAll.value = []
+    ElMessage.error('加载工资列表失败')
+  } finally {
+    listLoading.value = false
+  }
+}
+
+function applyStaffSearch() {
+  searchKeywordApplied.value = (staffSearchInput.value || '').trim()
+  pivotPage.value = 1
+}
+
+/** 与 renyuanjibenziliao 表头一致:紧凑;保留字号便于密列阅读 */
+function pivotHeaderCellStyle() {
+  return { padding: '0px', fontSize: '12px', textAlign: 'center' }
+}
+
+function pivotCellStyle({ column }) {
+  const base = { padding: '0px', fontSize: '12px', textAlign: 'center' }
+  const prop = column?.property
+  if (!prop || !String(prop).startsWith('day')) return base
+  const m = /^day(\d+)$/.exec(prop)
+  if (!m) return base
+  const d = Number(m[1])
+  if (d > daysInSelectedMonth.value) {
+    return {
+      ...base,
+      background: 'var(--el-fill-color-lighter)',
+    }
+  }
+  return base
+}
+
+/**
+ * 从表头列解析 1~31 日列;非日列(员工编号/姓名等)返回 0
+ * property 在部分环境可能为空,用 label 补救(日列表头为 "1"~"31")
+ */
+function getDayIndexFromTableColumn(column) {
+  if (!column) return 0
+  const prop = String(column.property ?? column.prop ?? '')
+  const pm = /^day(\d+)$/.exec(prop)
+  if (pm) {
+    const d = Number(pm[1])
+    return d >= 1 && d <= 31 ? d : 0
+  }
+  const lab = column.label
+  if (lab == null) return 0
+  const ls = String(lab).trim()
+  if (!/^\d{1,2}$/.test(ls)) return 0
+  const d = Number(ls)
+  return d >= 1 && d <= 31 ? d : 0
+}
+
+let pivotDblOpenLock = 0
+function openSalaryDetailForRowDay(row, dayNum) {
+  if (!row || dayNum < 1) return
+  const t = Date.now()
+  if (t - pivotDblOpenLock < 400) return
+  pivotDblOpenLock = t
+  const no = String(row['员工编号'] ?? '').trim()
+  if (!no || no === '—') {
+    ElMessage.warning('缺少员工编号')
+    return
+  }
+  const month = String(selectedMonth.value || '').trim()
+  if (!/^\d{4}-\d{2}$/.test(month)) return
+  const dateStr = `${month}-${String(dayNum).padStart(2, '0')}`
+  detailContext.value = {
+    staff_no: no,
+    date: dateStr,
+    big_process: String(selectedBigProcess.value || '').trim(),
+  }
+  detailVisible.value = true
+  loadDetailRows()
+}
+
+/** 有金额时日格上双击,直接带日 d 打开,避免 el-table 未带上 column */
+function onDayCellDblClick(row, d) {
+  if (!row) return
+  if (d > daysInSelectedMonth.value) return
+  if (row['_has' + d]) {
+    openSalaryDetailForRowDay(row, d)
+  }
+  /* 无 data 的提示交 onPivotDblOpenDetail,避免与 cell-dblclick 各弹一次 */
+}
+
+/**
+ * 双击行/单元格:有 column 时按日列/姓名列处理;日格金额优先走 onDayCellDblClick 以免 column 异常
+ * cell + row 会重复触发,防抖在 openSalaryDetailForRowDay
+ */
+function onPivotDblOpenDetail(row, column) {
+  if (!row) return
+  const dim = daysInSelectedMonth.value
+  const dCol = getDayIndexFromTableColumn(column)
+  if (dCol >= 1) {
+    if (dCol > dim) return
+    if (row['_has' + dCol]) {
+      openSalaryDetailForRowDay(row, dCol)
+    } else {
+      ElMessage.info('该日无工资数据,无法查看明细')
+    }
+    return
+  }
+  for (let d = 1; d <= dim; d++) {
+    if (row['_has' + d]) {
+      openSalaryDetailForRowDay(row, d)
+      return
+    }
+  }
+  ElMessage.info('该员工在当月无日工资数据,无法查看明细')
+}
+
+async function loadDetailRows() {
+  const { staff_no, date } = detailContext.value
+  if (!staff_no || !date) return
+  detailLoading.value = true
+  detailRows.value = []
+  try {
+    const res = await GetStaffSalaryDetail({ staff_no, date })
+    if (res?.code === 0) {
+      const raw = res.data
+      detailRows.value = Array.isArray(raw) ? raw : raw?.list ?? []
+    } else {
+      detailRows.value = []
+    }
+  } catch (e) {
+    console.error(e)
+    detailRows.value = []
+    ElMessage.error('加载明细失败')
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+function onDetailClosed() {
+  detailRows.value = []
+  detailContext.value = { staff_no: '', date: '', big_process: '' }
+}
+
+onMounted(() => {
+  loadLeftMenu()
+})
+</script>
+
+<style scoped>
+:deep(.el-table td .cell) {
+  line-height: 20px !important;
+}
+.search {
+  margin-left: 0 !important;
+  margin-right: 10px !important;
+}
+.bt {
+  margin-left: 2px !important;
+  padding: 3px !important;
+  font-size: 12px;
+}
+.gva-table-box {
+  padding: 0 !important;
+}
+/* 在 tabs + 顶栏 下与 admin-box 类似占满,避免主区上白下灰、侧栏树下方大块留白(与 ProductProcess 根节点一致) */
+.yunyin-hr-split.salary-list-page {
+  display: flex !important;
+  flex-direction: column !important;
+  box-sizing: border-box !important;
+  width: 100%;
+  max-width: 100%;
+  min-height: calc(100vh - 200px);
+  overflow: hidden !important;
+  background: var(--el-bg-color, #fff);
+}
+.salary-list-page > :deep(.arco-layout) {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.salary-list-page :deep(.arco-layout-has-sider) {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+.salary-list-page :deep(.arco-layout-sider) {
+  background: var(--el-fill-color-light) !important;
+  border-right: 1px solid var(--el-border-color-lighter);
+  align-self: stretch;
+  min-height: 0;
+}
+.salary-list-page :deep(.arco-layout-sider-children) {
+  padding-top: 0 !important;
+  padding-bottom: 0 !important;
+  height: 100% !important;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.salary-list-page :deep(.arco-layout-content) {
+  padding-top: 0 !important;
+  padding-bottom: 0 !important;
+  flex: 1;
+  min-width: 0;
+  min-height: 0;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background: var(--el-bg-color, #fff);
+}
+.salary-list-page .yunyin-main-block {
+  box-sizing: border-box;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background: var(--el-bg-color, #fff);
+  --el-main-padding: 0 12px 12px 12px;
+  padding: var(--el-main-padding) !important;
+}
+.salary-list-page .gva-table-box {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+/* 与人力页 renyuan 一致,但本页要铺满侧栏:取消 420 上限,树在 .salary-tree-scroll 内滚 */
+.staff-tree-panel {
+  min-height: 260px;
+  max-height: 420px;
+  overflow: auto;
+}
+.salary-tree-panel-fill {
+  min-height: 0 !important;
+  max-height: none !important;
+  height: 100%;
+  flex: 1 1 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  box-sizing: border-box;
+  padding: 8px 6px 8px 8px;
+}
+.salary-tree-h3 {
+  margin: 0 0 6px;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  flex-shrink: 0;
+}
+.salary-tree-scroll {
+  flex: 1 1 0;
+  min-height: 0;
+  overflow: auto;
+}
+.salary-tree-empty {
+  flex-shrink: 0;
+}
+.salary-main-placeholder {
+  flex: 1 1 0;
+  min-height: 200px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: var(--el-bg-color, #fff);
+}
+.yunyin-page-header :deep(.arco-layout-header) {
+  padding: 12px 10px;
+  box-sizing: border-box;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  background: var(--el-bg-color, #fff);
+  flex-shrink: 0;
+}
+/* 左侧树:默认正文色;选中(highlight-current)为红色 */
+:deep(.salary-menu-tree .el-tree-node__content) {
+  color: var(--el-text-color-regular);
+  font-weight: normal;
+}
+:deep(.salary-menu-tree .el-tree-node__content:hover) {
+  background-color: var(--el-fill-color) !important;
+}
+:deep(.salary-menu-tree .el-tree-node.is-current > .el-tree-node__content) {
+  color: var(--el-color-danger);
+  background-color: var(--el-color-danger-light-9) !important;
+  font-weight: 600;
+}
+:deep(.salary-menu-tree .el-tree-node.is-current .tree-node-label) {
+  color: var(--el-color-danger);
+}
+.salary-day-cell {
+  display: block;
+  min-height: 18px;
+}
+.salary-day-cell.is-outside {
+  color: var(--el-text-color-placeholder);
+}
+
+/* 选中行背景与 renyuanjibenziliao 一致 #ff80ff;悬停/斑马/固定列仍保持该色 */
+.salary-pivot-table {
+  --el-table-current-row-bg-color: #ff80ff;
+}
+.salary-pivot-table :deep(.el-table__body tr.current-row) > td,
+.salary-pivot-table :deep(.el-table__fixed-body-wrapper .el-table__body tr.current-row) > td {
+  background: #ff80ff !important;
+}
+.salary-pivot-table :deep(.el-table__body tr.el-table__row.current-row:hover > td.el-table__cell),
+.salary-pivot-table :deep(.el-table__body tr.el-table__row.hover-row.current-row > td.el-table__cell),
+.salary-pivot-table :deep(.el-table__body tr.el-table__row--striped.current-row:hover > td.el-table__cell),
+.salary-pivot-table :deep(.el-table__body tr.el-table__row--striped.hover-row.current-row > td.el-table__cell),
+.salary-pivot-table
+  :deep(.el-table__fixed-body-wrapper .el-table__body tr.el-table__row.current-row:hover > td.el-table__cell),
+.salary-pivot-table
+  :deep(.el-table__fixed-body-wrapper .el-table__body tr.el-table__row.hover-row.current-row > td.el-table__cell) {
+  background: #ff80ff !important;
+}
+</style>
+
+<style>
+/* 工资明细弹窗:加宽、加高;append-to-body 单独块便于命中 .el-dialog */
+.salary-detail-dialog.el-dialog {
+  width: min(1680px, 94vw) !important;
+  max-width: 98vw !important;
+  margin-top: 2vh;
+}
+.salary-detail-dialog .el-dialog__header {
+  padding: 12px 16px 8px;
+}
+.salary-detail-dialog .el-dialog__body {
+  padding: 0 12px 16px;
+  box-sizing: border-box;
+  /* 不整条 body 再滚,仅表格内 tbody 区域滚动(由 el-table 的 height 控制) */
+  overflow: hidden;
+}
+.salary-detail-table-wrap {
+  width: 100%;
+  min-height: 0;
+  overflow: hidden;
+}
+:deep(.salary-detail-table .el-table__body-wrapper) {
+  -webkit-overflow-scrolling: touch;
+}
+</style>

File diff ditekan karena terlalu besar
+ 538 - 188
src/view/yunyin/product/list.vue


+ 26 - 7
src/view/yunyin/renliziyuan/GroupManagement.vue

@@ -195,6 +195,16 @@
                   />
                 </el-select>
               </el-form-item>
+              <el-form-item label="职位" required>
+                <el-select
+                  v-model="changeTeamForm['职位']"
+                  placeholder="请选择职位"
+                  clearable
+                  class="add-member-select"
+                >
+                  <el-option v-for="p in positionOptions" :key="p" :label="p" :value="p" />
+                </el-select>
+              </el-form-item>
             </el-form>
             <template #footer>
               <el-button @click="changeTeamVisible = false">取消</el-button>
@@ -567,6 +577,7 @@ const changeTeamForm = reactive({
   生产工序: '',
   设备编组: '',
   UniqId: undefined,
+  职位: '成员',
 })
 
 const changeTeamStaffHint = computed(() => {
@@ -576,7 +587,7 @@ const changeTeamStaffHint = computed(() => {
   const c = String(r['员工编号'] ?? '').trim()
   if (n && c) return `将「${n}」(${c})调整到所选工序与小组`
   if (n) return `将「${n}」调整到所选工序与小组`
-  return '请选择目标工序与小组'
+  return '请选择目标工序、小组与职位'
 })
 
 const teamOptionsForChange = computed(() => {
@@ -651,10 +662,11 @@ const openChangeTeamDialog = async (row) => {
     changeTeamForm['设备编组'] = f['设备编组'] ?? ''
     syncUniqIdFromChangeForm()
   } else {
-    changeTeamForm['生产工序'] = ''
-    changeTeamForm['设备编组'] = ''
-    changeTeamForm.UniqId = undefined
+    changeTeamForm['生产工序'] = String(row['生产工序'] ?? '').trim()
+    changeTeamForm['设备编组'] = String(row['设备编组'] ?? '').trim()
+    syncUniqIdFromChangeForm()
   }
+  changeTeamForm['职位'] = String(row['职位'] ?? '成员').trim() || '成员'
   changeTeamVisible.value = true
 }
 
@@ -663,6 +675,7 @@ const resetChangeTeamForm = () => {
   changeTeamForm['生产工序'] = ''
   changeTeamForm['设备编组'] = ''
   changeTeamForm.UniqId = undefined
+  changeTeamForm['职位'] = '成员'
 }
 
 const submitChangeTeam = async () => {
@@ -681,11 +694,17 @@ const submitChangeTeam = async () => {
     ElMessage.warning('请选择设备编组')
     return
   }
+  const newPos = String(changeTeamForm['职位'] ?? '').trim() || '成员'
+  if (!newPos) {
+    ElMessage.warning('请选择职位')
+    return
+  }
   syncUniqIdFromChangeForm()
   const rowTeam = String(row['设备编组'] ?? '').trim()
   const rowGx = String(row['生产工序'] ?? '').trim()
-  if (rowTeam && rowGx && rowTeam === team && rowGx === gx) {
-    ElMessage.warning('目标小组与当前相同,无需更换')
+  const rowPos = String(row['职位'] ?? '').trim() || '成员'
+  if (rowTeam && rowGx && rowTeam === team && rowGx === gx && rowPos === newPos) {
+    ElMessage.warning('目标小组与职位与当前相同,无需提交')
     return
   }
   changeTeamSubmitting.value = true
@@ -697,7 +716,7 @@ const submitChangeTeam = async () => {
       team_name: team,
       staff_no: String(row['员工编号'] ?? ''),
       staff_name: String(row['员工姓名'] ?? ''),
-      position: String(row['职位'] ?? ''),
+      position: newPos,
       sys_id: userStore.userInfo?.nickName ?? '',
     })
     if (res?.code === 0) {

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini