|
@@ -73,7 +73,7 @@
|
|
|
:need-page="true"
|
|
:need-page="true"
|
|
|
:page-size="20"
|
|
:page-size="20"
|
|
|
:page-sizes="[10, 20, 50]"
|
|
:page-sizes="[10, 20, 50]"
|
|
|
- :cache-key="$tableCacheKey('hr-attendance-daily-v3')"
|
|
|
|
|
|
|
+ :cache-key="$tableCacheKey('hr-attendance-daily-v9')"
|
|
|
>
|
|
>
|
|
|
<template v-slot:toolbar>
|
|
<template v-slot:toolbar>
|
|
|
<!-- 后端暂无导入接口:点击如实提示,缺口已转后端改造清单 -->
|
|
<!-- 后端暂无导入接口:点击如实提示,缺口已转后端改造清单 -->
|
|
@@ -88,15 +88,57 @@
|
|
|
</el-link>
|
|
</el-link>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
- <template slot="statusText" slot-scope="{ row }">
|
|
|
|
|
- <span>{{ row.statusText }}</span>
|
|
|
|
|
|
|
+ <template slot="statusNameList" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 出勤情况:当日状态名称(如 正常/迟到/旷工),tag 一行一个、左对齐(2026-11-03 用户要求排列整齐) -->
|
|
|
|
|
+ <div v-for="(name, index) in row.statusNameList" :key="index" class="status-tag-line">
|
|
|
|
|
+ <el-tag size="mini" :type="attendanceStatusTagType(name)" class="punch-tag">
|
|
|
|
|
+ {{ name }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template slot="inStatus" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 上班·打卡情况:punchRecords 中 punchType=IN 记录的 statusName(后端中文直取) -->
|
|
|
|
|
+ <template v-if="row.inPunches.length">
|
|
|
|
|
+ <el-tag
|
|
|
|
|
+ v-for="(item, index) in row.inPunches"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ size="mini"
|
|
|
|
|
+ :type="punchStatusTagType(item)"
|
|
|
|
|
+ class="punch-tag"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ item.statusText }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template slot="inType" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 上班·打卡类型:记录的 sourceTypeName(移动端/考勤设备/补卡),纯文本逐行(2026-11-03 用户要求不用标签) -->
|
|
|
|
|
+ <div v-for="(item, index) in row.inPunches" :key="index" class="punch-time-line">
|
|
|
|
|
+ {{ item.sourceType }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template slot="inTime" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 上班·打卡时间:多条记录逐行展示,接口未返回则留空 -->
|
|
|
|
|
+ <div v-for="(item, index) in row.inPunches" :key="index" class="punch-time-line">
|
|
|
|
|
+ {{ item.punchTime }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template slot="inAddress" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 上班·打卡地点:punchRecords 中 punchType=IN 记录的 address,逐行展示,未返回则留空 -->
|
|
|
|
|
+ <div v-for="(item, index) in row.inPunches" :key="index" class="punch-time-line">
|
|
|
|
|
+ {{ item.address }}
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
- <template slot="punchStatuses" slot-scope="{ row }">
|
|
|
|
|
- <template v-if="row.punchStatuses.length">
|
|
|
|
|
|
|
+ <template slot="outStatus" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 下班·打卡情况:punchRecords 中 punchType=OUT 记录的 statusName -->
|
|
|
|
|
+ <template v-if="row.outPunches.length">
|
|
|
<el-tag
|
|
<el-tag
|
|
|
- v-for="item in row.punchStatuses"
|
|
|
|
|
- :key="item.punchRecordId"
|
|
|
|
|
|
|
+ v-for="(item, index) in row.outPunches"
|
|
|
|
|
+ :key="index"
|
|
|
size="mini"
|
|
size="mini"
|
|
|
:type="punchStatusTagType(item)"
|
|
:type="punchStatusTagType(item)"
|
|
|
class="punch-tag"
|
|
class="punch-tag"
|
|
@@ -106,6 +148,27 @@
|
|
|
</template>
|
|
</template>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
|
|
+ <template slot="outType" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 下班·打卡类型:记录的 sourceTypeName,纯文本逐行(2026-11-03 用户要求不用标签) -->
|
|
|
|
|
+ <div v-for="(item, index) in row.outPunches" :key="index" class="punch-time-line">
|
|
|
|
|
+ {{ item.sourceType }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template slot="outTime" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 下班·打卡时间:多条记录逐行展示,接口未返回则留空 -->
|
|
|
|
|
+ <div v-for="(item, index) in row.outPunches" :key="index" class="punch-time-line">
|
|
|
|
|
+ {{ item.punchTime }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template slot="outAddress" slot-scope="{ row }">
|
|
|
|
|
+ <!-- 下班·打卡地点:punchRecords 中 punchType=OUT 记录的 address,逐行展示,未返回则留空 -->
|
|
|
|
|
+ <div v-for="(item, index) in row.outPunches" :key="index" class="punch-time-line">
|
|
|
|
|
+ {{ item.address }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
<template slot="empty">
|
|
<template slot="empty">
|
|
|
<el-empty description="暂无数据" />
|
|
<el-empty description="暂无数据" />
|
|
|
</template>
|
|
</template>
|
|
@@ -121,10 +184,10 @@
|
|
|
>
|
|
>
|
|
|
<el-descriptions v-if="detailRow" :column="2" border size="small">
|
|
<el-descriptions v-if="detailRow" :column="2" border size="small">
|
|
|
<el-descriptions-item label="姓名">{{ detailRow.name }}</el-descriptions-item>
|
|
<el-descriptions-item label="姓名">{{ detailRow.name }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="工号">{{ detailRow.employeeNo }}</el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="所属部门">{{ detailRow.department }}</el-descriptions-item>
|
|
|
|
|
|
|
+ <el-descriptions-item label="岗位">{{ detailRow.postName }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="所属部门">{{ detailRow.deptName }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="日期">{{ detailRow.attendanceDate }}</el-descriptions-item>
|
|
<el-descriptions-item label="日期">{{ detailRow.attendanceDate }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="出勤情况" :span="2">{{ detailRow.statusText }}</el-descriptions-item>
|
|
|
|
|
|
|
+ <el-descriptions-item label="打卡情况" :span="2">{{ detailRow.statusText }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="计划上班">{{ detailRow.scheduledStart }}</el-descriptions-item>
|
|
<el-descriptions-item label="计划上班">{{ detailRow.scheduledStart }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="计划下班">{{ detailRow.scheduledEnd }}</el-descriptions-item>
|
|
<el-descriptions-item label="计划下班">{{ detailRow.scheduledEnd }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="上班打卡时间">{{ detailRow.actualIn }}</el-descriptions-item>
|
|
<el-descriptions-item label="上班打卡时间">{{ detailRow.actualIn }}</el-descriptions-item>
|
|
@@ -141,7 +204,7 @@
|
|
|
<el-descriptions-item label="出差(分钟)">{{ minuteText(detailRow.tripMinutes) }}</el-descriptions-item>
|
|
<el-descriptions-item label="出差(分钟)">{{ minuteText(detailRow.tripMinutes) }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="加班(分钟)">{{ minuteText(detailRow.overtimeMinutes) }}</el-descriptions-item>
|
|
<el-descriptions-item label="加班(分钟)">{{ minuteText(detailRow.overtimeMinutes) }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="夜班(分钟)">{{ minuteText(detailRow.nightMinutes) }}</el-descriptions-item>
|
|
<el-descriptions-item label="夜班(分钟)">{{ minuteText(detailRow.nightMinutes) }}</el-descriptions-item>
|
|
|
- <el-descriptions-item label="计算状态">{{ calculationStatusText(detailRow.calculationStatus) }}</el-descriptions-item>
|
|
|
|
|
|
|
+ <el-descriptions-item label="计算状态">{{ detailRow.calculationStatusName }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="最近核算时间">{{ detailRow.lastCalcTime }}</el-descriptions-item>
|
|
<el-descriptions-item label="最近核算时间">{{ detailRow.lastCalcTime }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
</el-descriptions>
|
|
|
|
|
|
|
@@ -150,7 +213,7 @@
|
|
|
<el-table :data="detailRow.punchStatuses" size="small" border>
|
|
<el-table :data="detailRow.punchStatuses" size="small" border>
|
|
|
<el-table-column prop="punchTime" label="打卡时间" min-width="150" />
|
|
<el-table-column prop="punchTime" label="打卡时间" min-width="150" />
|
|
|
<el-table-column prop="punchMethod" label="打卡方式" min-width="100" />
|
|
<el-table-column prop="punchMethod" label="打卡方式" min-width="100" />
|
|
|
- <el-table-column prop="punchType" label="打卡类型" min-width="120" />
|
|
|
|
|
|
|
+ <el-table-column prop="punchTypeName" label="打卡类型" min-width="120" />
|
|
|
<el-table-column prop="statusText" label="打卡情况" min-width="90" />
|
|
<el-table-column prop="statusText" label="打卡情况" min-width="90" />
|
|
|
<el-table-column prop="sourceType" label="数据来源" min-width="90" />
|
|
<el-table-column prop="sourceType" label="数据来源" min-width="90" />
|
|
|
</el-table>
|
|
</el-table>
|
|
@@ -165,7 +228,6 @@
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
import * as XLSX from 'xlsx';
|
|
import * as XLSX from 'xlsx';
|
|
|
-import { loadEmployeeBriefs } from '@/api/organization';
|
|
|
|
|
import { getCurrentOrgId, getOrgOptions } from '@/utils/current-org';
|
|
import { getCurrentOrgId, getOrgOptions } from '@/utils/current-org';
|
|
|
import { loadOrganizationList } from '@/utils/org-list-cache';
|
|
import { loadOrganizationList } from '@/utils/org-list-cache';
|
|
|
import {
|
|
import {
|
|
@@ -178,12 +240,6 @@ import {
|
|
|
adaptDailyAttendance,
|
|
adaptDailyAttendance,
|
|
|
} from '@/api/hr/attendance';
|
|
} from '@/api/hr/attendance';
|
|
|
|
|
|
|
|
-const CALCULATION_STATUS_LABEL = {
|
|
|
|
|
- PROVISIONAL: '实时暂态',
|
|
|
|
|
- CALCULATED: '最终结果',
|
|
|
|
|
- LOCKED: '月结锁定',
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
/* 日期筛选默认当月:本月 1 号 ~ 今天(2026-10-30 用户指定默认查当月) */
|
|
/* 日期筛选默认当月:本月 1 号 ~ 今天(2026-10-30 用户指定默认查当月) */
|
|
|
function currentMonthRange() {
|
|
function currentMonthRange() {
|
|
|
const now = new Date();
|
|
const now = new Date();
|
|
@@ -205,24 +261,39 @@ export default {
|
|
|
dateRange: currentMonthRange(),
|
|
dateRange: currentMonthRange(),
|
|
|
},
|
|
},
|
|
|
filterOrgOptions: [],
|
|
filterOrgOptions: [],
|
|
|
- /* 组织树全量部门(扁平,带 level 缩进);按选中机构子树过滤见 filterDeptOptions */
|
|
|
|
|
allDeptOptions: [],
|
|
allDeptOptions: [],
|
|
|
orgTreeNodes: [],
|
|
orgTreeNodes: [],
|
|
|
columns: [
|
|
columns: [
|
|
|
{ type: 'index', label: '序号', width: 60, align: 'center', headerAlign: 'center', fixed: 'left' },
|
|
{ type: 'index', label: '序号', width: 60, align: 'center', headerAlign: 'center', fixed: 'left' },
|
|
|
- /* /hr/attendance/daily 与 /main/user/batch 均未返回机构字段:列保留、值留空,缺口转后端清单 */
|
|
|
|
|
- { prop: 'organization', label: '所属机构', minWidth: 180 },
|
|
|
|
|
|
|
+ /* 日期列不排序:后端 /hr/attendance/daily 无排序参数,服务端分页下前端排序只能排当前页,会误导 */
|
|
|
|
|
+ { prop: 'attendanceDate', label: '日期', minWidth: 110, fixed: 'left' },
|
|
|
{ prop: 'name', label: '姓名', minWidth: 100, slot: 'name', fixed: 'left' },
|
|
{ prop: 'name', label: '姓名', minWidth: 100, slot: 'name', fixed: 'left' },
|
|
|
- { prop: 'department', label: '所属部门', minWidth: 140 },
|
|
|
|
|
- { prop: 'attendanceDate', label: '日期', minWidth: 110, sortable: 'custom' },
|
|
|
|
|
- { prop: 'statusText', label: '出勤情况', minWidth: 120, slot: 'statusText' },
|
|
|
|
|
- { prop: 'punchTypes', label: '打卡类型', minWidth: 130 },
|
|
|
|
|
- /* 接口未返回打卡地址:列保留、值留空,缺口转后端改造清单 */
|
|
|
|
|
- { prop: 'punchAddress', label: '打卡地址', minWidth: 140 },
|
|
|
|
|
- { prop: 'punchStatuses', label: '打卡情况', minWidth: 120, slot: 'punchStatuses' },
|
|
|
|
|
- { prop: 'actualIn', label: '上班打卡时间', minWidth: 160 },
|
|
|
|
|
- { prop: 'actualOut', label: '下班打卡时间', minWidth: 160 },
|
|
|
|
|
- /* 接口未返回工作地址:列保留、值留空,缺口转后端改造清单 */
|
|
|
|
|
|
|
+ { prop: 'postName', label: '岗位', minWidth: 130 },
|
|
|
|
|
+ { prop: 'groupName', label: '所属机构', minWidth: 220 },
|
|
|
|
|
+ { prop: 'deptName', label: '所属部门', minWidth: 140 },
|
|
|
|
|
+ /* 出勤情况:当日状态名称集合,后端 statusNameList 直取(2026-11-02 用户指定字段);tag 逐行左对齐 */
|
|
|
|
|
+ { prop: 'statusNameList', label: '出勤情况', minWidth: 130, align: 'left', headerAlign: 'left', slot: 'statusNameList' },
|
|
|
|
|
+ {
|
|
|
|
|
+ label: '上班',
|
|
|
|
|
+ align: 'center',
|
|
|
|
|
+ children: [
|
|
|
|
|
+ { prop: 'inStatus', label: '打卡状态', minWidth: 100, slot: 'inStatus' },
|
|
|
|
|
+ { prop: 'inType', label: '打卡类型', minWidth: 110, slot: 'inType' },
|
|
|
|
|
+ { prop: 'inTime', label: '打卡时间', minWidth:90, slot: 'inTime' },
|
|
|
|
|
+ /* 打卡地点:punchRecords 内 address 直取(2026-11-03 用户指定),分组各自取 IN/OUT 记录 */
|
|
|
|
|
+ { prop: 'inAddress', label: '打卡地点', minWidth:90, slot: 'inAddress' },
|
|
|
|
|
+ ],
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ label: '下班',
|
|
|
|
|
+ align: 'center',
|
|
|
|
|
+ children: [
|
|
|
|
|
+ { prop: 'outStatus', label: '打卡状态', minWidth: 100, slot: 'outStatus' },
|
|
|
|
|
+ { prop: 'outType', label: '打卡类型', minWidth: 110, slot: 'outType' },
|
|
|
|
|
+ { prop: 'outTime', label: '打卡时间', minWidth: 90, slot: 'outTime' },
|
|
|
|
|
+ { prop: 'outAddress', label: '打卡地点', minWidth:90, slot: 'outAddress' },
|
|
|
|
|
+ ],
|
|
|
|
|
+ },
|
|
|
{ prop: 'workAddress', label: '工作地址', minWidth: 140 },
|
|
{ prop: 'workAddress', label: '工作地址', minWidth: 140 },
|
|
|
],
|
|
],
|
|
|
/* 当前页数据快照:本地 excel 导出用(后端暂无导出接口) */
|
|
/* 当前页数据快照:本地 excel 导出用(后端暂无导出接口) */
|
|
@@ -236,8 +307,6 @@ export default {
|
|
|
this.loadDeptOptions();
|
|
this.loadDeptOptions();
|
|
|
},
|
|
},
|
|
|
computed: {
|
|
computed: {
|
|
|
- /* 部门选项:选中机构时只列该机构子树下的部门(不含机构自身);
|
|
|
|
|
- 机构不在组织树里时展示全量部门,保证部门筛选仍可用 */
|
|
|
|
|
filterDeptOptions() {
|
|
filterDeptOptions() {
|
|
|
if (!this.filter.orgId) return this.allDeptOptions;
|
|
if (!this.filter.orgId) return this.allDeptOptions;
|
|
|
const node = this.findOrgNode(this.orgTreeNodes, this.filter.orgId);
|
|
const node = this.findOrgNode(this.orgTreeNodes, this.filter.orgId);
|
|
@@ -304,25 +373,29 @@ export default {
|
|
|
dateStart: (this.filter.dateRange || [])[0] || undefined,
|
|
dateStart: (this.filter.dateRange || [])[0] || undefined,
|
|
|
dateEnd: (this.filter.dateRange || [])[1] || undefined,
|
|
dateEnd: (this.filter.dateRange || [])[1] || undefined,
|
|
|
});
|
|
});
|
|
|
- /* 接口只回 userId:姓名/工号/部门走主数据批量接口补齐(≤200/次,带缓存) */
|
|
|
|
|
- const briefs = await loadEmployeeBriefs((data.list || []).map((item) => item.userId));
|
|
|
|
|
- this.currentRows = (data.list || []).map((raw) =>
|
|
|
|
|
- adaptDailyAttendance(raw, briefs.get(String(raw.userId ?? ''))),
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ /* 姓名/岗位/部门/机构/状态/地址等字段均直取 daily 接口返回(后端已补中文与姓名岗位字段) */
|
|
|
|
|
+ this.currentRows = (data.list || []).map((raw) => adaptDailyAttendance(raw));
|
|
|
return { list: this.currentRows, count: data.count };
|
|
return { list: this.currentRows, count: data.count };
|
|
|
},
|
|
},
|
|
|
punchStatusTagType(item) {
|
|
punchStatusTagType(item) {
|
|
|
- if (item.statusText === '正常') return 'success';
|
|
|
|
|
|
|
+ /* 文案与色值均按后端中文字段判断:statusName=有效/已作废/待审批,sourceType=移动端/考勤设备/补卡 */
|
|
|
|
|
+ if (item.sourceType === '补卡' || item.statusText === '已作废') return 'danger';
|
|
|
|
|
+ if (item.statusText === '有效') return 'success';
|
|
|
if (item.statusText === '待审批') return 'warning';
|
|
if (item.statusText === '待审批') return 'warning';
|
|
|
- if (item.statusText === '补卡' || item.statusText === '无效') return 'danger';
|
|
|
|
|
|
|
+ return 'info';
|
|
|
|
|
+ },
|
|
|
|
|
+ /* 出勤情况 tag 颜色:按后端状态中文字段判断(正常=绿,迟到/早退/缺卡类=橙,旷工=红,其他=灰) */
|
|
|
|
|
+ attendanceStatusTagType(name) {
|
|
|
|
|
+ const text = String(name || '');
|
|
|
|
|
+ if (!text) return 'info';
|
|
|
|
|
+ if (text.includes('正常')) return 'success';
|
|
|
|
|
+ if (text.includes('旷工')) return 'danger';
|
|
|
|
|
+ if (text.includes('迟到') || text.includes('早退') || text.includes('缺')) return 'warning';
|
|
|
return 'info';
|
|
return 'info';
|
|
|
},
|
|
},
|
|
|
minuteText(value) {
|
|
minuteText(value) {
|
|
|
return value === null || value === undefined ? '' : String(value);
|
|
return value === null || value === undefined ? '' : String(value);
|
|
|
},
|
|
},
|
|
|
- calculationStatusText(status) {
|
|
|
|
|
- return CALCULATION_STATUS_LABEL[status] || status || '';
|
|
|
|
|
- },
|
|
|
|
|
viewRow(row) {
|
|
viewRow(row) {
|
|
|
this.detailRow = row;
|
|
this.detailRow = row;
|
|
|
this.detailVisible = true;
|
|
this.detailVisible = true;
|
|
@@ -331,35 +404,53 @@ export default {
|
|
|
this.$message.info('导入考勤明细的后端接口暂未提供,请后端补充导入接口');
|
|
this.$message.info('导入考勤明细的后端接口暂未提供,请后端补充导入接口');
|
|
|
},
|
|
},
|
|
|
/* 后端暂无导出接口:先导出当前页已加载数据(xlsx,SheetJS 本地生成,与员工档案导入模板同款);
|
|
/* 后端暂无导出接口:先导出当前页已加载数据(xlsx,SheetJS 本地生成,与员工档案导入模板同款);
|
|
|
- 全量导出待后端补接口。注意 SheetJS 社区版只支持写 .xlsx,写不了老格式 .xls(BIFF8)。 */
|
|
|
|
|
|
|
+ 全量导出待后端补接口。注意 SheetJS 社区版只支持写 .xlsx,写不了老格式 .xls(BIFF8)。
|
|
|
|
|
+ 表头两级(2026-11-02 起与列表一致):上班/下班 横跨三列合并,其余列纵向合并两行。 */
|
|
|
onExport() {
|
|
onExport() {
|
|
|
if (!this.currentRows.length) {
|
|
if (!this.currentRows.length) {
|
|
|
this.$message.warning('暂无考勤明细可导出');
|
|
this.$message.warning('暂无考勤明细可导出');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+ /* 列顺序与页面一致:序号/日期/姓名/岗位/所属机构/所属部门/出勤情况;
|
|
|
|
|
+ 上班/下班 各四列(打卡状态/打卡类型/打卡时间/打卡地点,地点=punchRecords.address,2026-11-03 起随页面) */
|
|
|
|
|
+ const singleCols = ['序号', '日期', '姓名', '岗位', '所属机构', '所属部门', '出勤情况'];
|
|
|
const header = [
|
|
const header = [
|
|
|
- '序号', '姓名', '所属机构', '所属部门', '日期', '出勤情况', '打卡类型',
|
|
|
|
|
- '打卡地址', '打卡情况', '上班打卡时间', '下班打卡时间', '工作地址',
|
|
|
|
|
|
|
+ [...singleCols, '上班', '', '', '', '下班', '', '', '', '工作地址'],
|
|
|
|
|
+ ['', '', '', '', '', '', '', '打卡状态', '打卡类型', '打卡时间', '打卡地点', '打卡状态', '打卡类型', '打卡时间', '打卡地点', ''],
|
|
|
];
|
|
];
|
|
|
|
|
+ const joinText = (list) => list.filter(Boolean).join(' ');
|
|
|
const body = this.currentRows.map((row, index) => [
|
|
const body = this.currentRows.map((row, index) => [
|
|
|
index + 1,
|
|
index + 1,
|
|
|
- row.name,
|
|
|
|
|
- row.organization,
|
|
|
|
|
- row.department,
|
|
|
|
|
row.attendanceDate,
|
|
row.attendanceDate,
|
|
|
- row.statusText,
|
|
|
|
|
- row.punchTypes,
|
|
|
|
|
- row.punchAddress,
|
|
|
|
|
- row.punchStatuses.map((item) => item.statusText).join(' '),
|
|
|
|
|
- row.actualIn,
|
|
|
|
|
- row.actualOut,
|
|
|
|
|
|
|
+ row.name,
|
|
|
|
|
+ row.postName,
|
|
|
|
|
+ row.groupName,
|
|
|
|
|
+ row.deptName,
|
|
|
|
|
+ row.statusNameList.join('、'),
|
|
|
|
|
+ joinText(row.inPunches.map((item) => item.statusText)),
|
|
|
|
|
+ joinText(row.inPunches.map((item) => item.sourceType)),
|
|
|
|
|
+ joinText(row.inPunches.map((item) => item.punchTime)),
|
|
|
|
|
+ joinText(row.inPunches.map((item) => item.address)),
|
|
|
|
|
+ joinText(row.outPunches.map((item) => item.statusText)),
|
|
|
|
|
+ joinText(row.outPunches.map((item) => item.sourceType)),
|
|
|
|
|
+ joinText(row.outPunches.map((item) => item.punchTime)),
|
|
|
|
|
+ joinText(row.outPunches.map((item) => item.address)),
|
|
|
row.workAddress,
|
|
row.workAddress,
|
|
|
]);
|
|
]);
|
|
|
- const sheet = XLSX.utils.aoa_to_sheet([header, ...body], { cellDates: true });
|
|
|
|
|
- /* 中文按 2 字宽估列宽,表头不被截断;日期/时间列给宽一点 */
|
|
|
|
|
- sheet['!cols'] = header.map((name, index) => ({
|
|
|
|
|
- wch: index === 4 || index >= 9 ? Math.max(20, name.length * 2) : Math.max(10, name.length * 2),
|
|
|
|
|
- }));
|
|
|
|
|
|
|
+ const sheet = XLSX.utils.aoa_to_sheet([...header, ...body], { cellDates: true });
|
|
|
|
|
+ /* 合并:序号~出勤情况(0~6 列)与 工作地址(15 列)纵向合并两行;
|
|
|
|
|
+ 上班 7~10 列、下班 11~14 列横向合并第一行 */
|
|
|
|
|
+ const merges = [];
|
|
|
|
|
+ [0, 1, 2, 3, 4, 5, 6, 15].forEach((c) => merges.push({ s: { r: 0, c }, e: { r: 1, c } }));
|
|
|
|
|
+ merges.push({ s: { r: 0, c: 7 }, e: { r: 0, c: 10 } });
|
|
|
|
|
+ merges.push({ s: { r: 0, c: 11 }, e: { r: 0, c: 14 } });
|
|
|
|
|
+ sheet['!merges'] = merges;
|
|
|
|
|
+ /* 中文按 2 字宽估列宽,表头不被截断;日期/时间/地址列给宽一点 */
|
|
|
|
|
+ const widthNames = [...singleCols, '打卡状态', '打卡类型', '打卡时间', '打卡地点', '打卡状态', '打卡类型', '打卡时间', '打卡地点', '工作地址'];
|
|
|
|
|
+ sheet['!cols'] = widthNames.map((name, index) => {
|
|
|
|
|
+ const wide = index === 1 || index === 10 || index === 14 || index >= 15;
|
|
|
|
|
+ return { wch: Math.max(wide ? 20 : 12, name.length * 2) };
|
|
|
|
|
+ });
|
|
|
const book = XLSX.utils.book_new();
|
|
const book = XLSX.utils.book_new();
|
|
|
XLSX.utils.book_append_sheet(book, sheet, '考勤明细');
|
|
XLSX.utils.book_append_sheet(book, sheet, '考勤明细');
|
|
|
XLSX.writeFile(book, `考勤明细-${new Date().toISOString().slice(0, 10)}.xlsx`);
|
|
XLSX.writeFile(book, `考勤明细-${new Date().toISOString().slice(0, 10)}.xlsx`);
|
|
@@ -378,6 +469,15 @@ export default {
|
|
|
margin-right: 6px;
|
|
margin-right: 6px;
|
|
|
margin-bottom: 2px;
|
|
margin-bottom: 2px;
|
|
|
}
|
|
}
|
|
|
|
|
+/* 上下班分组「打卡时间」列:多条打卡记录逐行展示 */
|
|
|
|
|
+.punch-time-line {
|
|
|
|
|
+ line-height: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+/* 出勤情况列:tag 一行一个、强制左对齐(防全局/表头样式带偏) */
|
|
|
|
|
+.status-tag-line {
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ line-height: 26px;
|
|
|
|
|
+}
|
|
|
.punch-records {
|
|
.punch-records {
|
|
|
margin-top: 16px;
|
|
margin-top: 16px;
|
|
|
h4 {
|
|
h4 {
|