|
@@ -5,17 +5,56 @@
|
|
|
<div>
|
|
<div>
|
|
|
<el-form
|
|
<el-form
|
|
|
class="demo-form-inline"
|
|
class="demo-form-inline"
|
|
|
- @keyup.enter="applyStaffSearch"
|
|
|
|
|
|
|
+ @submit.prevent="handleStaffQueryClick"
|
|
|
>
|
|
>
|
|
|
<el-form-item>
|
|
<el-form-item>
|
|
|
- <el-input
|
|
|
|
|
- v-model="staffSearchInput"
|
|
|
|
|
- placeholder="查询订单编号、员工编号或员工姓名"
|
|
|
|
|
- clearable
|
|
|
|
|
- class="search"
|
|
|
|
|
- style="width: 260px"
|
|
|
|
|
- />
|
|
|
|
|
- <el-button type="primary" icon="search" class="bt" @click="applyStaffSearch">查询</el-button>
|
|
|
|
|
|
|
+ <div ref="searchWrapRef" class="workorder-search-wrap search">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="staffSearchInput"
|
|
|
|
|
+ placeholder="查询订单编号、员工编号或员工姓名"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ style="width: 260px"
|
|
|
|
|
+ @keyup.enter="handleStaffQueryClick"
|
|
|
|
|
+ @input="onSearchKeywordInput"
|
|
|
|
|
+ @clear="closeWorkOrderDropdown"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 挂到 body,避免被 layout overflow 裁切;内容与工序产量核查一致 -->
|
|
|
|
|
+ <Teleport to="body">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-show="workOrderDropdownVisible && workOrderOptions.length"
|
|
|
|
|
+ ref="workOrderDropdownRef"
|
|
|
|
|
+ class="salary-workorder-dropdown-panel"
|
|
|
|
|
+ :style="workOrderDropdownStyle"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="workorder-suggest-header">
|
|
|
|
|
+ <span>订单编号</span>
|
|
|
|
|
+ <span>生产款号</span>
|
|
|
|
|
+ <span>款式</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="workorder-dropdown-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="item in workOrderOptions"
|
|
|
|
|
+ :key="item.订单编号"
|
|
|
|
|
+ class="workorder-suggest-row"
|
|
|
|
|
+ @click="onPickWorkOrder(item)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="workorder-suggest-no" :title="item.订单编号">{{ item.订单编号 }}</span>
|
|
|
|
|
+ <span class="workorder-suggest-style" :title="item.生产款号">{{ item.生产款号 }}</span>
|
|
|
|
|
+ <span class="workorder-suggest-name" :title="item.款式">{{ item.款式 }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Teleport>
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ icon="search"
|
|
|
|
|
+ class="bt"
|
|
|
|
|
+ :loading="workOrderSuggestLoading"
|
|
|
|
|
+ @click="handleStaffQueryClick"
|
|
|
|
|
+ >
|
|
|
|
|
+ 查询
|
|
|
|
|
+ </el-button>
|
|
|
<el-button type="primary" icon="search" class="bt" @click="openProcessProductionDialog">工序产量核查</el-button>
|
|
<el-button type="primary" icon="search" class="bt" @click="openProcessProductionDialog">工序产量核查</el-button>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
@@ -106,11 +145,41 @@
|
|
|
<el-table-column
|
|
<el-table-column
|
|
|
label="员工姓名"
|
|
label="员工姓名"
|
|
|
prop="员工姓名"
|
|
prop="员工姓名"
|
|
|
- min-width="100"
|
|
|
|
|
|
|
+ min-width="120"
|
|
|
align="center"
|
|
align="center"
|
|
|
fixed="left"
|
|
fixed="left"
|
|
|
show-overflow-tooltip
|
|
show-overflow-tooltip
|
|
|
- />
|
|
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <TableColumnFilterHeader
|
|
|
|
|
+ label="员工姓名"
|
|
|
|
|
+ v-model="pivotStaffNameFilter"
|
|
|
|
|
+ :options="pivotStaffNameFilterOptions"
|
|
|
|
|
+ :active="isPivotStaffNameFilterActive"
|
|
|
|
|
+ @select-all="selectAllPivotStaffNameFilter"
|
|
|
|
|
+ @clear="clearPivotStaffNameFilter"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ label="月份"
|
|
|
|
|
+ prop="月份"
|
|
|
|
|
+ width="80"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ fixed="left"
|
|
|
|
|
+ show-overflow-tooltip
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <TableColumnFilterHeader
|
|
|
|
|
+ label="月份"
|
|
|
|
|
+ v-model="pivotMonthFilter"
|
|
|
|
|
+ :options="pivotMonthFilterOptions"
|
|
|
|
|
+ :active="isPivotMonthFilterActive"
|
|
|
|
|
+ @select-all="selectAllPivotMonthFilter"
|
|
|
|
|
+ @clear="clearPivotMonthFilter"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
<el-table-column
|
|
<el-table-column
|
|
|
v-for="d in DAY_INDEXES"
|
|
v-for="d in DAY_INDEXES"
|
|
|
:key="'d' + d"
|
|
:key="'d' + d"
|
|
@@ -124,10 +193,10 @@
|
|
|
<span
|
|
<span
|
|
|
class="salary-day-cell"
|
|
class="salary-day-cell"
|
|
|
:class="{
|
|
:class="{
|
|
|
- 'is-outside': d > daysInSelectedMonth,
|
|
|
|
|
|
|
+ 'is-outside': d > (row.daysInMonth || daysInSelectedMonth),
|
|
|
}"
|
|
}"
|
|
|
:title="
|
|
:title="
|
|
|
- d <= daysInSelectedMonth && row['_has' + d]
|
|
|
|
|
|
|
+ d <= (row.daysInMonth || daysInSelectedMonth) && row['_has' + d]
|
|
|
? '双击可查看该日工资明细'
|
|
? '双击可查看该日工资明细'
|
|
|
: undefined
|
|
: undefined
|
|
|
"
|
|
"
|
|
@@ -422,7 +491,7 @@
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { Layout, LayoutHeader, LayoutSider, LayoutContent } from '@arco-design/web-vue'
|
|
import { Layout, LayoutHeader, LayoutSider, LayoutContent } from '@arco-design/web-vue'
|
|
|
-import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
|
|
|
|
|
|
+import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import {
|
|
import {
|
|
|
GetReportingWorkMonth,
|
|
GetReportingWorkMonth,
|
|
@@ -430,6 +499,7 @@ import {
|
|
|
GetStaffSalaryDetail,
|
|
GetStaffSalaryDetail,
|
|
|
UpdateReportingWork,
|
|
UpdateReportingWork,
|
|
|
DeleteReportingWork,
|
|
DeleteReportingWork,
|
|
|
|
|
+ WorkOrderList,
|
|
|
} from '@/api/mes/job'
|
|
} from '@/api/mes/job'
|
|
|
import ProcessProduction from '@/view/SalaryManage/ProcessProduction.vue'
|
|
import ProcessProduction from '@/view/SalaryManage/ProcessProduction.vue'
|
|
|
import TableColumnFilterHeader from '@/view/SalaryManage/TableColumnFilterHeader.vue'
|
|
import TableColumnFilterHeader from '@/view/SalaryManage/TableColumnFilterHeader.vue'
|
|
@@ -472,6 +542,118 @@ const staffSearchInput = ref('')
|
|
|
/** 点击「查询」后传给接口的 search */
|
|
/** 点击「查询」后传给接口的 search */
|
|
|
const searchKeywordApplied = ref('')
|
|
const searchKeywordApplied = ref('')
|
|
|
|
|
|
|
|
|
|
+/** 订单模糊下拉(与 ProcessProduction 一致:订单编号/生产款号/款式) */
|
|
|
|
|
+const searchWrapRef = ref(null)
|
|
|
|
|
+const workOrderDropdownRef = ref(null)
|
|
|
|
|
+const workOrderDropdownVisible = ref(false)
|
|
|
|
|
+const workOrderOptions = ref([])
|
|
|
|
|
+const workOrderSuggestLoading = ref(false)
|
|
|
|
|
+const workOrderDropdownStyle = ref({
|
|
|
|
|
+ top: '0px',
|
|
|
|
|
+ left: '0px',
|
|
|
|
|
+ width: '680px',
|
|
|
|
|
+})
|
|
|
|
|
+let workOrderSuggestSeq = 0
|
|
|
|
|
+let searchInputTimer = null
|
|
|
|
|
+
|
|
|
|
|
+const updateWorkOrderDropdownPosition = () => {
|
|
|
|
|
+ const el = searchWrapRef.value
|
|
|
|
|
+ if (!el) return
|
|
|
|
|
+ const rect = el.getBoundingClientRect()
|
|
|
|
|
+ workOrderDropdownStyle.value = {
|
|
|
|
|
+ top: `${Math.round(rect.bottom + 4)}px`,
|
|
|
|
|
+ left: `${Math.round(rect.left)}px`,
|
|
|
|
|
+ width: `${Math.min(680, Math.max(360, window.innerWidth - rect.left - 16))}px`,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const openWorkOrderDropdown = () => {
|
|
|
|
|
+ if (!workOrderOptions.value.length) {
|
|
|
|
|
+ workOrderDropdownVisible.value = false
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ updateWorkOrderDropdownPosition()
|
|
|
|
|
+ workOrderDropdownVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const closeWorkOrderDropdown = () => {
|
|
|
|
|
+ workOrderDropdownVisible.value = false
|
|
|
|
|
+ workOrderOptions.value = []
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onSearchKeywordInput = () => {
|
|
|
|
|
+ clearTimeout(searchInputTimer)
|
|
|
|
|
+ searchInputTimer = setTimeout(async () => {
|
|
|
|
|
+ const q = String(staffSearchInput.value || '').trim()
|
|
|
|
|
+ if (!q) {
|
|
|
|
|
+ closeWorkOrderDropdown()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ await remoteSearchWorkOrder(q)
|
|
|
|
|
+ openWorkOrderDropdown()
|
|
|
|
|
+ }, 300)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onDocClick = (event) => {
|
|
|
|
|
+ const t = event.target
|
|
|
|
|
+ if (searchWrapRef.value?.contains(t) || workOrderDropdownRef.value?.contains(t)) return
|
|
|
|
|
+ workOrderDropdownVisible.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const remoteSearchWorkOrder = async (query) => {
|
|
|
|
|
+ const q = String(query || '').trim()
|
|
|
|
|
+ if (!q) {
|
|
|
|
|
+ workOrderOptions.value = []
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const seq = ++workOrderSuggestSeq
|
|
|
|
|
+ workOrderSuggestLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await WorkOrderList({ search: q, page: 1, limit: 20 })
|
|
|
|
|
+ if (seq !== workOrderSuggestSeq) return
|
|
|
|
|
+ workOrderOptions.value = res?.code === 0 && Array.isArray(res.data?.data) ? res.data.data : []
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(error)
|
|
|
|
|
+ workOrderOptions.value = []
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (seq === workOrderSuggestSeq) {
|
|
|
|
|
+ workOrderSuggestLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 选中工单后按订单编号查工资列表 */
|
|
|
|
|
+const onPickWorkOrder = (item) => {
|
|
|
|
|
+ const no = String(item?.订单编号 || '').trim()
|
|
|
|
|
+ if (!no) return
|
|
|
|
|
+ staffSearchInput.value = no
|
|
|
|
|
+ workOrderDropdownVisible.value = false
|
|
|
|
|
+ runStaffSearch(no)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 查询 / 回车:优先走工单模糊匹配;
|
|
|
|
|
+ * 仅 1 条直接查,多条展开下拉;无工单匹配则按原逻辑用关键词查员工编号/姓名
|
|
|
|
|
+ */
|
|
|
|
|
+const handleStaffQueryClick = async () => {
|
|
|
|
|
+ const q = String(staffSearchInput.value || '').trim()
|
|
|
|
|
+ if (!q) {
|
|
|
|
|
+ ElMessage.warning('请输入订单编号/员工编号/姓名进行查询')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ await remoteSearchWorkOrder(q)
|
|
|
|
|
+ if (workOrderOptions.value.length === 1) {
|
|
|
|
|
+ onPickWorkOrder(workOrderOptions.value[0])
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (workOrderOptions.value.length > 1) {
|
|
|
|
|
+ openWorkOrderDropdown()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ workOrderDropdownVisible.value = false
|
|
|
|
|
+ runStaffSearch(q)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 左侧树有日期,或顶部已点过查询(有 search)均可展示列表 */
|
|
/** 左侧树有日期,或顶部已点过查询(有 search)均可展示列表 */
|
|
|
const hasQueryCondition = computed(
|
|
const hasQueryCondition = computed(
|
|
|
() =>
|
|
() =>
|
|
@@ -614,7 +796,16 @@ const pickPivotGroup = (row) => {
|
|
|
return g && g !== '—' ? g : ''
|
|
return g && g !== '—' ? g : ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const pickPivotStaffName = (row) => {
|
|
|
|
|
+ const n = String(row?.员工姓名 ?? '').trim()
|
|
|
|
|
+ return n && n !== '—' ? n : ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const pickPivotMonth = (row) => String(row?.月份 ?? '').trim()
|
|
|
|
|
+
|
|
|
const pivotGroupFilterCtrl = createListColumnFilter(pivotRowsAll, pickPivotGroup)
|
|
const pivotGroupFilterCtrl = createListColumnFilter(pivotRowsAll, pickPivotGroup)
|
|
|
|
|
+const pivotStaffNameFilterCtrl = createListColumnFilter(pivotRowsAll, pickPivotStaffName)
|
|
|
|
|
+const pivotMonthFilterCtrl = createListColumnFilter(pivotRowsAll, pickPivotMonth, { sortDesc: true })
|
|
|
|
|
|
|
|
const detailOrderNoFilterCtrl = createListColumnFilter(detailRowsAll, (row) => String(row.订单编号 || '').trim())
|
|
const detailOrderNoFilterCtrl = createListColumnFilter(detailRowsAll, (row) => String(row.订单编号 || '').trim())
|
|
|
const detailStyleNoFilterCtrl = createListColumnFilter(detailRowsAll, (row) => String(row.生产款号 || '').trim())
|
|
const detailStyleNoFilterCtrl = createListColumnFilter(detailRowsAll, (row) => String(row.生产款号 || '').trim())
|
|
@@ -637,6 +828,30 @@ const {
|
|
|
apply: applyPivotGroupFilter,
|
|
apply: applyPivotGroupFilter,
|
|
|
} = pivotGroupFilterCtrl
|
|
} = pivotGroupFilterCtrl
|
|
|
|
|
|
|
|
|
|
+const {
|
|
|
|
|
+ selected: pivotStaffNameFilter,
|
|
|
|
|
+ options: pivotStaffNameFilterOptions,
|
|
|
|
|
+ isActive: isPivotStaffNameFilterActive,
|
|
|
|
|
+ selectAll: selectAllPivotStaffNameFilter,
|
|
|
|
|
+ clear: clearPivotStaffNameFilter,
|
|
|
|
|
+ apply: applyPivotStaffNameFilter,
|
|
|
|
|
+} = pivotStaffNameFilterCtrl
|
|
|
|
|
+
|
|
|
|
|
+const {
|
|
|
|
|
+ selected: pivotMonthFilter,
|
|
|
|
|
+ options: pivotMonthFilterOptions,
|
|
|
|
|
+ isActive: isPivotMonthFilterActive,
|
|
|
|
|
+ selectAll: selectAllPivotMonthFilter,
|
|
|
|
|
+ clear: clearPivotMonthFilter,
|
|
|
|
|
+ apply: applyPivotMonthFilter,
|
|
|
|
|
+} = pivotMonthFilterCtrl
|
|
|
|
|
+
|
|
|
|
|
+function selectAllPivotFilters() {
|
|
|
|
|
+ selectAllPivotGroupFilter()
|
|
|
|
|
+ selectAllPivotStaffNameFilter()
|
|
|
|
|
+ selectAllPivotMonthFilter()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const {
|
|
const {
|
|
|
selected: detailOrderNoFilter,
|
|
selected: detailOrderNoFilter,
|
|
|
options: detailOrderNoFilterOptions,
|
|
options: detailOrderNoFilterOptions,
|
|
@@ -721,7 +936,13 @@ watch(detailRowsAll, () => {
|
|
|
syncAllDetailFilters()
|
|
syncAllDetailFilters()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const pivotRowsFiltered = computed(() => applyPivotGroupFilter(pivotRowsAll.value))
|
|
|
|
|
|
|
+const pivotRowsFiltered = computed(() => {
|
|
|
|
|
+ let rows = pivotRowsAll.value
|
|
|
|
|
+ rows = applyPivotGroupFilter(rows)
|
|
|
|
|
+ rows = applyPivotStaffNameFilter(rows)
|
|
|
|
|
+ rows = applyPivotMonthFilter(rows)
|
|
|
|
|
+ return rows
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
const pagedPivotRows = computed(() => {
|
|
const pagedPivotRows = computed(() => {
|
|
|
const list = pivotRowsFiltered.value
|
|
const list = pivotRowsFiltered.value
|
|
@@ -736,10 +957,10 @@ watch([pivotRowsFiltered, pivotPageSize], ([arr, ps]) => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
watch(pivotRowsAll, () => {
|
|
watch(pivotRowsAll, () => {
|
|
|
- selectAllPivotGroupFilter()
|
|
|
|
|
|
|
+ selectAllPivotFilters()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-watch(pivotGroupFilter, () => {
|
|
|
|
|
|
|
+watch([pivotGroupFilter, pivotStaffNameFilter, pivotMonthFilter], () => {
|
|
|
pivotPage.value = 1
|
|
pivotPage.value = 1
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -764,6 +985,14 @@ function parseStaffKey(staffStr) {
|
|
|
return { staff_no: '', staff_name: s || '—' }
|
|
return { staff_no: '', staff_name: s || '—' }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 从日期串解析 YYYY-MM */
|
|
|
|
|
+function parseYearMonthFromDate(dateStr) {
|
|
|
|
|
+ const ds = String(dateStr ?? '').trim()
|
|
|
|
|
+ const m = ds.match(/^(\d{4})[-/.年](\d{1,2})/)
|
|
|
|
|
+ if (!m) return ''
|
|
|
|
|
+ return `${m[1]}-${String(Number(m[2])).padStart(2, '0')}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 从日期串解析为「日」,且必须属于 monthYm(YYYY-MM) */
|
|
/** 从日期串解析为「日」,且必须属于 monthYm(YYYY-MM) */
|
|
|
function parseDayInMonth(dateStr, monthYm) {
|
|
function parseDayInMonth(dateStr, monthYm) {
|
|
|
if (!dateStr || !monthYm) return -1
|
|
if (!dateStr || !monthYm) return -1
|
|
@@ -779,8 +1008,15 @@ function parseDayInMonth(dateStr, monthYm) {
|
|
|
return -1
|
|
return -1
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 从列表项解析小组名(兼容多种后端字段) */
|
|
|
|
|
-function pickGroupFromItem(item, fallbackGroup = '') {
|
|
|
|
|
|
|
+function daysCountInYm(monthYm) {
|
|
|
|
|
+ if (!monthYm || !/^\d{4}-\d{2}$/.test(monthYm)) return 31
|
|
|
|
|
+ const [y, m] = monthYm.split('-').map(Number)
|
|
|
|
|
+ if (!m || m < 1 || m > 12) return 31
|
|
|
|
|
+ return new Date(y, m, 0).getDate()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 优先取指定月份 children 上的小组 */
|
|
|
|
|
+function pickGroupFromItem(item, fallbackGroup = '', monthYm = '') {
|
|
|
const direct =
|
|
const direct =
|
|
|
item?.['小组'] ??
|
|
item?.['小组'] ??
|
|
|
item?.group ??
|
|
item?.group ??
|
|
@@ -789,65 +1025,110 @@ function pickGroupFromItem(item, fallbackGroup = '') {
|
|
|
item?.team_name
|
|
item?.team_name
|
|
|
if (direct != null && String(direct).trim()) return String(direct).trim()
|
|
if (direct != null && String(direct).trim()) return String(direct).trim()
|
|
|
for (const c of item?.children || []) {
|
|
for (const c of item?.children || []) {
|
|
|
|
|
+ if (monthYm && parseYearMonthFromDate(c['日期']) !== monthYm) continue
|
|
|
const g = c?.['小组'] ?? c?.group ?? c?.['设备编组'] ?? c?.['设备编号']
|
|
const g = c?.['小组'] ?? c?.group ?? c?.['设备编组'] ?? c?.['设备编号']
|
|
|
if (g != null && String(g).trim()) return String(g).trim()
|
|
if (g != null && String(g).trim()) return String(g).trim()
|
|
|
}
|
|
}
|
|
|
|
|
+ if (monthYm) {
|
|
|
|
|
+ for (const c of item?.children || []) {
|
|
|
|
|
+ const g = c?.['小组'] ?? c?.group ?? c?.['设备编组'] ?? c?.['设备编号']
|
|
|
|
|
+ if (g != null && String(g).trim()) return String(g).trim()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
return String(fallbackGroup || '').trim()
|
|
return String(fallbackGroup || '').trim()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 将 GetStaffSalaryList 转为一行一人、day1~day31 为当日工资
|
|
|
|
|
- * 同月同日多条 children 时金额相加
|
|
|
|
|
|
|
+ * 将 GetStaffSalaryList 转为透视行:同一员工若跨月则拆成多行
|
|
|
|
|
+ * 同月同日多条 children 时金额相加;月计为该月合计
|
|
|
*/
|
|
*/
|
|
|
-function buildPivotRows(list, monthYm, fallbackGroup = '') {
|
|
|
|
|
- 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()
|
|
|
|
|
- })()
|
|
|
|
|
|
|
+function buildPivotRows(list, preferredMonthYm, fallbackGroup = '') {
|
|
|
const out = []
|
|
const out = []
|
|
|
let i = 0
|
|
let i = 0
|
|
|
for (const item of list || []) {
|
|
for (const item of list || []) {
|
|
|
const p = parseStaffKey(item.staff)
|
|
const p = parseStaffKey(item.staff)
|
|
|
const baseNo = String(item['员工编号'] ?? p.staff_no ?? '').trim()
|
|
const baseNo = String(item['员工编号'] ?? p.staff_no ?? '').trim()
|
|
|
const baseName = String(item['员工姓名'] ?? p.staff_name ?? '').trim()
|
|
const baseName = String(item['员工姓名'] ?? p.staff_name ?? '').trim()
|
|
|
- const groupName = pickGroupFromItem(item, fallbackGroup)
|
|
|
|
|
- const sumByDay = {}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /** @type {Map<string, { sumByDay: Record<number, number>, monthTotal: number }>} */
|
|
|
|
|
+ const byMonth = new Map()
|
|
|
for (const c of item.children || []) {
|
|
for (const c of item.children || []) {
|
|
|
- const day = parseDayInMonth(c['日期'], monthYm)
|
|
|
|
|
- if (day < 1 || day > dim) continue
|
|
|
|
|
|
|
+ const ym = parseYearMonthFromDate(c['日期'])
|
|
|
|
|
+ if (!ym) continue
|
|
|
|
|
+ let bucket = byMonth.get(ym)
|
|
|
|
|
+ if (!bucket) {
|
|
|
|
|
+ bucket = { sumByDay: {}, monthTotal: 0 }
|
|
|
|
|
+ byMonth.set(ym, bucket)
|
|
|
|
|
+ }
|
|
|
|
|
+ const day = parseDayInMonth(c['日期'], ym)
|
|
|
const w = c['工资']
|
|
const w = c['工资']
|
|
|
if (w === null || w === undefined || w === '') continue
|
|
if (w === null || w === undefined || w === '') continue
|
|
|
const n = Number(w)
|
|
const n = Number(w)
|
|
|
if (Number.isNaN(n)) continue
|
|
if (Number.isNaN(n)) continue
|
|
|
- sumByDay[day] = (sumByDay[day] || 0) + n
|
|
|
|
|
|
|
+ if (day >= 1 && day <= 31) {
|
|
|
|
|
+ bucket.sumByDay[day] = (bucket.sumByDay[day] || 0) + n
|
|
|
|
|
+ }
|
|
|
|
|
+ bucket.monthTotal += n
|
|
|
}
|
|
}
|
|
|
- const row = {
|
|
|
|
|
- rowKey: `pv-${i++}-${baseNo}-${groupName || i}`,
|
|
|
|
|
- 小组: groupName || '—',
|
|
|
|
|
- 员工编号: baseNo || '—',
|
|
|
|
|
- 员工姓名: baseName || '—',
|
|
|
|
|
- // 添加月计列,使用接口返回的total_salary数据
|
|
|
|
|
- 月计: item.total_salary ? Number(item.total_salary).toFixed(2) : '0.00'
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 月份大的在前(如 9 在 8 前)
|
|
|
|
|
+ let monthKeys = [...byMonth.keys()].sort((a, b) => b.localeCompare(a))
|
|
|
|
|
+ if (!monthKeys.length) {
|
|
|
|
|
+ monthKeys =
|
|
|
|
|
+ preferredMonthYm && /^\d{4}-\d{2}$/.test(preferredMonthYm)
|
|
|
|
|
+ ? [preferredMonthYm]
|
|
|
|
|
+ : ['']
|
|
|
}
|
|
}
|
|
|
- for (let d = 1; d <= 31; d++) {
|
|
|
|
|
- const keyH = '_has' + d
|
|
|
|
|
- if (d > dim) {
|
|
|
|
|
- row['day' + d] = ''
|
|
|
|
|
- row[keyH] = false
|
|
|
|
|
- continue
|
|
|
|
|
|
|
+
|
|
|
|
|
+ for (const ym of monthKeys) {
|
|
|
|
|
+ const dim = daysCountInYm(ym)
|
|
|
|
|
+ const monthNum = ym ? String(Number(ym.split('-')[1])) : ''
|
|
|
|
|
+ const bucket = byMonth.get(ym) || { sumByDay: {}, monthTotal: 0 }
|
|
|
|
|
+ const groupName = pickGroupFromItem(item, fallbackGroup, ym)
|
|
|
|
|
+ const row = {
|
|
|
|
|
+ rowKey: `pv-${i++}-${baseNo}-${ym || 'na'}-${groupName || i}`,
|
|
|
|
|
+ 小组: groupName || '—',
|
|
|
|
|
+ 员工编号: baseNo || '—',
|
|
|
|
|
+ 员工姓名: baseName || '—',
|
|
|
|
|
+ 月份: monthNum,
|
|
|
|
|
+ monthYm: ym,
|
|
|
|
|
+ daysInMonth: dim,
|
|
|
|
|
+ 月计:
|
|
|
|
|
+ byMonth.size > 0
|
|
|
|
|
+ ? Number(bucket.monthTotal).toFixed(2)
|
|
|
|
|
+ : item.total_salary
|
|
|
|
|
+ ? Number(item.total_salary).toFixed(2)
|
|
|
|
|
+ : '0.00',
|
|
|
}
|
|
}
|
|
|
- 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
|
|
|
|
|
|
|
+ for (let d = 1; d <= 31; d++) {
|
|
|
|
|
+ const keyH = '_has' + d
|
|
|
|
|
+ if (!ym || d > dim) {
|
|
|
|
|
+ row['day' + d] = ''
|
|
|
|
|
+ row[keyH] = false
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ const v = bucket.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)
|
|
|
}
|
|
}
|
|
|
- out.push(row)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ // 全局按月份倒序:如 9 月全部在前,8 月全部在后
|
|
|
|
|
+ out.sort((a, b) => {
|
|
|
|
|
+ const ymA = String(a.monthYm || '')
|
|
|
|
|
+ const ymB = String(b.monthYm || '')
|
|
|
|
|
+ if (ymA !== ymB) return ymB.localeCompare(ymA)
|
|
|
|
|
+ const noA = String(a.员工编号 || '')
|
|
|
|
|
+ const noB = String(b.员工编号 || '')
|
|
|
|
|
+ if (noA !== noB) return noA.localeCompare(noB)
|
|
|
|
|
+ return String(a.小组 || '').localeCompare(String(b.小组 || ''), 'zh-CN')
|
|
|
|
|
+ })
|
|
|
return out
|
|
return out
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1191,9 +1472,9 @@ async function loadStaffList() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function applyStaffSearch() {
|
|
|
|
|
- const search = (staffSearchInput.value || '').trim()
|
|
|
|
|
- if (!search) {
|
|
|
|
|
|
|
+function runStaffSearch(search) {
|
|
|
|
|
+ const q = String(search || '').trim()
|
|
|
|
|
+ if (!q) {
|
|
|
ElMessage.warning('请输入订单编号/员工编号/姓名进行查询')
|
|
ElMessage.warning('请输入订单编号/员工编号/姓名进行查询')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
@@ -1201,24 +1482,29 @@ function applyStaffSearch() {
|
|
|
listQuery.value = { sys_rq: '', big_process: '', group: '' }
|
|
listQuery.value = { sys_rq: '', big_process: '', group: '' }
|
|
|
currentMenuKey.value = ''
|
|
currentMenuKey.value = ''
|
|
|
restoreMenuCurrentKey()
|
|
restoreMenuCurrentKey()
|
|
|
- searchKeywordApplied.value = search
|
|
|
|
|
|
|
+ searchKeywordApplied.value = q
|
|
|
pivotPage.value = 1
|
|
pivotPage.value = 1
|
|
|
loadStaffList()
|
|
loadStaffList()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function applyStaffSearch() {
|
|
|
|
|
+ handleStaffQueryClick()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 与 renyuanjibenziliao 表头一致:紧凑;保留字号便于密列阅读 */
|
|
/** 与 renyuanjibenziliao 表头一致:紧凑;保留字号便于密列阅读 */
|
|
|
function pivotHeaderCellStyle() {
|
|
function pivotHeaderCellStyle() {
|
|
|
return { padding: '0px', fontSize: '12px', textAlign: 'center' }
|
|
return { padding: '0px', fontSize: '12px', textAlign: 'center' }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function pivotCellStyle({ column }) {
|
|
|
|
|
|
|
+function pivotCellStyle({ row, column }) {
|
|
|
const base = { padding: '0px', fontSize: '12px', textAlign: 'center' }
|
|
const base = { padding: '0px', fontSize: '12px', textAlign: 'center' }
|
|
|
const prop = column?.property
|
|
const prop = column?.property
|
|
|
if (!prop || !String(prop).startsWith('day')) return base
|
|
if (!prop || !String(prop).startsWith('day')) return base
|
|
|
const m = /^day(\d+)$/.exec(prop)
|
|
const m = /^day(\d+)$/.exec(prop)
|
|
|
if (!m) return base
|
|
if (!m) return base
|
|
|
const d = Number(m[1])
|
|
const d = Number(m[1])
|
|
|
- if (d > daysInSelectedMonth.value) {
|
|
|
|
|
|
|
+ const dim = Number(row?.daysInMonth) || daysInSelectedMonth.value
|
|
|
|
|
+ if (d > dim) {
|
|
|
return {
|
|
return {
|
|
|
...base,
|
|
...base,
|
|
|
background: 'var(--el-fill-color-lighter)',
|
|
background: 'var(--el-fill-color-lighter)',
|
|
@@ -1259,8 +1545,10 @@ function openSalaryDetailForRowDay(row, dayNum) {
|
|
|
ElMessage.warning('缺少员工编号')
|
|
ElMessage.warning('缺少员工编号')
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- const monthYm = String(selectedMonthYm.value || '').trim()
|
|
|
|
|
|
|
+ const monthYm = String(row.monthYm || selectedMonthYm.value || '').trim()
|
|
|
if (!/^\d{4}-\d{2}$/.test(monthYm)) return
|
|
if (!/^\d{4}-\d{2}$/.test(monthYm)) return
|
|
|
|
|
+ const dim = Number(row.daysInMonth) || daysCountInYm(monthYm)
|
|
|
|
|
+ if (dayNum > dim) return
|
|
|
const dateStr = `${monthYm}-${String(dayNum).padStart(2, '0')}`
|
|
const dateStr = `${monthYm}-${String(dayNum).padStart(2, '0')}`
|
|
|
detailContext.value = {
|
|
detailContext.value = {
|
|
|
staff_no: no,
|
|
staff_no: no,
|
|
@@ -1275,7 +1563,8 @@ function openSalaryDetailForRowDay(row, dayNum) {
|
|
|
/** 有金额时日格上双击,直接带日 d 打开,避免 el-table 未带上 column */
|
|
/** 有金额时日格上双击,直接带日 d 打开,避免 el-table 未带上 column */
|
|
|
function onDayCellDblClick(row, d) {
|
|
function onDayCellDblClick(row, d) {
|
|
|
if (!row) return
|
|
if (!row) return
|
|
|
- if (d > daysInSelectedMonth.value) return
|
|
|
|
|
|
|
+ const dim = Number(row.daysInMonth) || daysInSelectedMonth.value
|
|
|
|
|
+ if (d > dim) return
|
|
|
if (row['_has' + d]) {
|
|
if (row['_has' + d]) {
|
|
|
openSalaryDetailForRowDay(row, d)
|
|
openSalaryDetailForRowDay(row, d)
|
|
|
}
|
|
}
|
|
@@ -1288,7 +1577,7 @@ function onDayCellDblClick(row, d) {
|
|
|
*/
|
|
*/
|
|
|
function onPivotDblOpenDetail(row, column) {
|
|
function onPivotDblOpenDetail(row, column) {
|
|
|
if (!row) return
|
|
if (!row) return
|
|
|
- const dim = daysInSelectedMonth.value
|
|
|
|
|
|
|
+ const dim = Number(row.daysInMonth) || daysInSelectedMonth.value
|
|
|
const dCol = getDayIndexFromTableColumn(column)
|
|
const dCol = getDayIndexFromTableColumn(column)
|
|
|
if (dCol >= 1) {
|
|
if (dCol >= 1) {
|
|
|
if (dCol > dim) return
|
|
if (dCol > dim) return
|
|
@@ -1542,6 +1831,16 @@ const getDetailSummaries = (param) => {
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
loadLeftMenu()
|
|
loadLeftMenu()
|
|
|
|
|
+ document.addEventListener('click', onDocClick)
|
|
|
|
|
+ window.addEventListener('resize', updateWorkOrderDropdownPosition)
|
|
|
|
|
+ window.addEventListener('scroll', updateWorkOrderDropdownPosition, true)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ document.removeEventListener('click', onDocClick)
|
|
|
|
|
+ window.removeEventListener('resize', updateWorkOrderDropdownPosition)
|
|
|
|
|
+ window.removeEventListener('scroll', updateWorkOrderDropdownPosition, true)
|
|
|
|
|
+ clearTimeout(searchInputTimer)
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -1553,6 +1852,11 @@ onMounted(() => {
|
|
|
margin-left: 0 !important;
|
|
margin-left: 0 !important;
|
|
|
margin-right: 10px !important;
|
|
margin-right: 10px !important;
|
|
|
}
|
|
}
|
|
|
|
|
+.workorder-search-wrap {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ vertical-align: middle;
|
|
|
|
|
+}
|
|
|
.bt {
|
|
.bt {
|
|
|
margin-left: 2px !important;
|
|
margin-left: 2px !important;
|
|
|
padding: 3px !important;
|
|
padding: 3px !important;
|
|
@@ -1746,6 +2050,66 @@ onMounted(() => {
|
|
|
</style>
|
|
</style>
|
|
|
|
|
|
|
|
<style>
|
|
<style>
|
|
|
|
|
+/* 订单模糊下拉:Teleport 到 body,避免被页面 overflow 裁切 */
|
|
|
|
|
+.salary-workorder-dropdown-panel {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ z-index: 4000;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-suggest-header {
|
|
|
|
|
+ padding: 8px 12px 6px;
|
|
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-dropdown-list {
|
|
|
|
|
+ max-height: 280px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-suggest-header,
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-suggest-row {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 118px minmax(220px, 1fr) minmax(140px, 180px);
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-suggest-row {
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-suggest-row:hover {
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+}
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-suggest-no {
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-suggest-style {
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+.salary-workorder-dropdown-panel .workorder-suggest-name {
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* 工资明细弹窗:加宽、加高;append-to-body 单独块便于命中 .el-dialog */
|
|
/* 工资明细弹窗:加宽、加高;append-to-body 单独块便于命中 .el-dialog */
|
|
|
.salary-detail-dialog.el-dialog {
|
|
.salary-detail-dialog.el-dialog {
|
|
|
width: min(1680px, 94vw) !important;
|
|
width: min(1680px, 94vw) !important;
|