|
|
@@ -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>
|