|
|
@@ -1,17 +1,24 @@
|
|
|
<template>
|
|
|
<div class="ele-body">
|
|
|
<el-card shadow="never" class="daily-card">
|
|
|
- <!-- 筛选区:姓名 / 所属部门 / 日期(与设计图一致) -->
|
|
|
+ <!-- 筛选区:所属机构 / 所属部门 / 姓名 / 日期 -->
|
|
|
<el-form inline :model="filter" class="daily-filter" @submit.native.prevent>
|
|
|
- <el-form-item label="姓名">
|
|
|
- <el-input
|
|
|
- v-model="filter.employeeKeyword"
|
|
|
- placeholder="姓名模糊/工号精确"
|
|
|
+ <el-form-item label="所属机构">
|
|
|
+ <el-select
|
|
|
+ v-model="filter.orgId"
|
|
|
+ placeholder="请选择"
|
|
|
clearable
|
|
|
- style="width: 200px"
|
|
|
- @keyup.enter.native="search"
|
|
|
- @clear="search"
|
|
|
- />
|
|
|
+ filterable
|
|
|
+ style="width: 220px"
|
|
|
+ @change="onOrgChange"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in filterOrgOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="所属部门">
|
|
|
<el-select
|
|
|
@@ -23,13 +30,23 @@
|
|
|
@change="search"
|
|
|
>
|
|
|
<el-option
|
|
|
- v-for="item in filterOrgOptions"
|
|
|
+ v-for="item in filterDeptOptions"
|
|
|
:key="item.value"
|
|
|
:label="item.label"
|
|
|
:value="item.value"
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
+ <el-form-item label="姓名">
|
|
|
+ <el-input
|
|
|
+ v-model="filter.employeeKeyword"
|
|
|
+ placeholder="姓名模糊/工号精确"
|
|
|
+ clearable
|
|
|
+ style="width: 200px"
|
|
|
+ @keyup.enter.native="search"
|
|
|
+ @clear="search"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
<el-form-item label="日期">
|
|
|
<el-date-picker
|
|
|
v-model="filter.dateRange"
|
|
|
@@ -56,7 +73,7 @@
|
|
|
:need-page="true"
|
|
|
:page-size="20"
|
|
|
:page-sizes="[10, 20, 50]"
|
|
|
- :cache-key="$tableCacheKey('hr-attendance-daily')"
|
|
|
+ :cache-key="$tableCacheKey('hr-attendance-daily-v3')"
|
|
|
>
|
|
|
<template v-slot:toolbar>
|
|
|
<!-- 后端暂无导入接口:点击如实提示,缺口已转后端改造清单 -->
|
|
|
@@ -65,7 +82,10 @@
|
|
|
</template>
|
|
|
|
|
|
<template slot="name" slot-scope="{ row }">
|
|
|
- <span>{{ row.name }}</span>
|
|
|
+ <!-- 点姓名看详情(替代原操作列「查看」入口) -->
|
|
|
+ <el-link type="primary" :underline="false" @click="viewRow(row)">
|
|
|
+ {{ row.name || '—' }}
|
|
|
+ </el-link>
|
|
|
</template>
|
|
|
|
|
|
<template slot="statusText" slot-scope="{ row }">
|
|
|
@@ -86,10 +106,6 @@
|
|
|
</template>
|
|
|
</template>
|
|
|
|
|
|
- <template slot="action" slot-scope="{ row }">
|
|
|
- <el-link type="primary" :underline="false" @click="viewRow(row)">查看</el-link>
|
|
|
- </template>
|
|
|
-
|
|
|
<template slot="empty">
|
|
|
<el-empty description="暂无数据" />
|
|
|
</template>
|
|
|
@@ -148,8 +164,15 @@
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
+import * as XLSX from 'xlsx';
|
|
|
import { loadEmployeeBriefs } from '@/api/organization';
|
|
|
import { getCurrentOrgId, getOrgOptions } from '@/utils/current-org';
|
|
|
+import { loadOrganizationList } from '@/utils/org-list-cache';
|
|
|
+import {
|
|
|
+ buildOrganizationTree,
|
|
|
+ flattenOrganizationTree,
|
|
|
+ collectDescendantIds,
|
|
|
+} from '@/utils/organization-tree';
|
|
|
import {
|
|
|
getDailyAttendanceDetailPage,
|
|
|
adaptDailyAttendance,
|
|
|
@@ -161,18 +184,34 @@ const CALCULATION_STATUS_LABEL = {
|
|
|
LOCKED: '月结锁定',
|
|
|
};
|
|
|
|
|
|
+/* 日期筛选默认当月:本月 1 号 ~ 今天(2026-10-30 用户指定默认查当月) */
|
|
|
+function currentMonthRange() {
|
|
|
+ const now = new Date();
|
|
|
+ const pad = (value) => String(value).padStart(2, '0');
|
|
|
+ const start = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-01`;
|
|
|
+ const end = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
|
+ return [start, end];
|
|
|
+}
|
|
|
+
|
|
|
export default {
|
|
|
name: 'AttendanceDaily',
|
|
|
data() {
|
|
|
return {
|
|
|
filter: {
|
|
|
+ /* 所属机构:登录缓存 orgVO,默认当前机构 */
|
|
|
+ orgId: getCurrentOrgId() || '',
|
|
|
+ deptId: '',
|
|
|
employeeKeyword: '',
|
|
|
- deptId: getCurrentOrgId() || '',
|
|
|
- dateRange: null,
|
|
|
+ dateRange: currentMonthRange(),
|
|
|
},
|
|
|
filterOrgOptions: [],
|
|
|
+ /* 组织树全量部门(扁平,带 level 缩进);按选中机构子树过滤见 filterDeptOptions */
|
|
|
+ allDeptOptions: [],
|
|
|
+ orgTreeNodes: [],
|
|
|
columns: [
|
|
|
- { type: 'index', label: '序号', width: 60, align: 'center', headerAlign: 'center' },
|
|
|
+ { type: 'index', label: '序号', width: 60, align: 'center', headerAlign: 'center', fixed: 'left' },
|
|
|
+ /* /hr/attendance/daily 与 /main/user/batch 均未返回机构字段:列保留、值留空,缺口转后端清单 */
|
|
|
+ { prop: 'organization', label: '所属机构', minWidth: 180 },
|
|
|
{ prop: 'name', label: '姓名', minWidth: 100, slot: 'name', fixed: 'left' },
|
|
|
{ prop: 'department', label: '所属部门', minWidth: 140 },
|
|
|
{ prop: 'attendanceDate', label: '日期', minWidth: 110, sortable: 'custom' },
|
|
|
@@ -185,7 +224,6 @@ export default {
|
|
|
{ prop: 'actualOut', label: '下班打卡时间', minWidth: 160 },
|
|
|
/* 接口未返回工作地址:列保留、值留空,缺口转后端改造清单 */
|
|
|
{ prop: 'workAddress', label: '工作地址', minWidth: 140 },
|
|
|
- { prop: 'action', label: '操作', width: 80, slot: 'action', fixed: 'right' },
|
|
|
],
|
|
|
/* 当前页数据快照:本地 excel 导出用(后端暂无导出接口) */
|
|
|
currentRows: [],
|
|
|
@@ -195,33 +233,74 @@ export default {
|
|
|
},
|
|
|
created() {
|
|
|
this.loadFilterOrgOptions();
|
|
|
+ this.loadDeptOptions();
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ /* 部门选项:选中机构时只列该机构子树下的部门(不含机构自身);
|
|
|
+ 机构不在组织树里时展示全量部门,保证部门筛选仍可用 */
|
|
|
+ filterDeptOptions() {
|
|
|
+ if (!this.filter.orgId) return this.allDeptOptions;
|
|
|
+ const node = this.findOrgNode(this.orgTreeNodes, this.filter.orgId);
|
|
|
+ if (!node) return this.allDeptOptions;
|
|
|
+ const ids = new Set(collectDescendantIds(node));
|
|
|
+ ids.delete(String(this.filter.orgId));
|
|
|
+ return this.allDeptOptions.filter((item) => ids.has(String(item.value)));
|
|
|
+ },
|
|
|
},
|
|
|
methods: {
|
|
|
search() {
|
|
|
this.$refs.table?.reload?.({ page: 1 });
|
|
|
},
|
|
|
+ /* 切换机构:清掉已选部门(部门选项随机构子树变化),重查 */
|
|
|
+ onOrgChange() {
|
|
|
+ this.filter.deptId = '';
|
|
|
+ this.search();
|
|
|
+ },
|
|
|
reset() {
|
|
|
this.filter = {
|
|
|
+ orgId: getCurrentOrgId() || '',
|
|
|
+ deptId: '',
|
|
|
employeeKeyword: '',
|
|
|
- deptId: getCurrentOrgId() || '',
|
|
|
- dateRange: null,
|
|
|
+ dateRange: currentMonthRange(),
|
|
|
};
|
|
|
this.search();
|
|
|
},
|
|
|
- /* 筛选「所属部门」选项:登录缓存机构列表,与其他页面口径一致 */
|
|
|
+ /* 筛选「所属机构」选项:登录缓存机构列表,与其他页面口径一致 */
|
|
|
loadFilterOrgOptions() {
|
|
|
this.filterOrgOptions = getOrgOptions().map((item) => ({
|
|
|
value: item.id,
|
|
|
label: item.name || '当前机构',
|
|
|
}));
|
|
|
},
|
|
|
+ /* 部门选项:组织树扁平化(带层级缩进),失败不阻塞列表查询 */
|
|
|
+ async loadDeptOptions() {
|
|
|
+ try {
|
|
|
+ const list = await loadOrganizationList();
|
|
|
+ this.orgTreeNodes = buildOrganizationTree(list || []);
|
|
|
+ this.allDeptOptions = flattenOrganizationTree(this.orgTreeNodes).map((item) => ({
|
|
|
+ value: item.id,
|
|
|
+ label: `${' '.repeat(item.level || 0)}${item.name || ''}`,
|
|
|
+ }));
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[考勤明细] 组织列表加载失败,部门筛选暂不可用:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ findOrgNode(nodes, orgId) {
|
|
|
+ for (const node of nodes) {
|
|
|
+ if (String(node.id) === String(orgId)) return node;
|
|
|
+ const hit = this.findOrgNode(node.children || [], orgId);
|
|
|
+ if (hit) return hit;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ },
|
|
|
async fetchPage({ page, limit }) {
|
|
|
- // GET /hr/attendance/daily
|
|
|
+ // GET /hr/attendance/daily —— 接口只有一个组织参数 deptId:
|
|
|
+ // 选了部门传部门 id,否则传所属机构 id(机构也是组织树节点,服务端按数据权限校验)
|
|
|
const data = await getDailyAttendanceDetailPage({
|
|
|
pageNum: page,
|
|
|
size: limit,
|
|
|
employeeKeyword: this.filter.employeeKeyword || undefined,
|
|
|
- deptId: this.filter.deptId || undefined,
|
|
|
+ deptId: this.filter.deptId || this.filter.orgId || undefined,
|
|
|
dateStart: (this.filter.dateRange || [])[0] || undefined,
|
|
|
dateEnd: (this.filter.dateRange || [])[1] || undefined,
|
|
|
});
|
|
|
@@ -251,44 +330,39 @@ export default {
|
|
|
onImport() {
|
|
|
this.$message.info('导入考勤明细的后端接口暂未提供,请后端补充导入接口');
|
|
|
},
|
|
|
- /* 后端暂无导出接口:先导出当前页已加载数据(CSV),全量导出待后端补接口 */
|
|
|
+ /* 后端暂无导出接口:先导出当前页已加载数据(xlsx,SheetJS 本地生成,与员工档案导入模板同款);
|
|
|
+ 全量导出待后端补接口。注意 SheetJS 社区版只支持写 .xlsx,写不了老格式 .xls(BIFF8)。 */
|
|
|
onExport() {
|
|
|
if (!this.currentRows.length) {
|
|
|
this.$message.warning('暂无考勤明细可导出');
|
|
|
return;
|
|
|
}
|
|
|
const header = [
|
|
|
- '序号', '姓名', '所属部门', '日期', '出勤情况', '打卡类型',
|
|
|
+ '序号', '姓名', '所属机构', '所属部门', '日期', '出勤情况', '打卡类型',
|
|
|
'打卡地址', '打卡情况', '上班打卡时间', '下班打卡时间', '工作地址',
|
|
|
];
|
|
|
- const lines = this.currentRows.map((row, index) =>
|
|
|
- [
|
|
|
- index + 1,
|
|
|
- row.name,
|
|
|
- row.department,
|
|
|
- row.attendanceDate,
|
|
|
- row.statusText,
|
|
|
- row.punchTypes,
|
|
|
- row.punchAddress,
|
|
|
- row.punchStatuses.map((item) => item.statusText).join(' '),
|
|
|
- row.actualIn,
|
|
|
- row.actualOut,
|
|
|
- row.workAddress,
|
|
|
- ]
|
|
|
- .map((cell) => `"${String(cell ?? '').replace(/"/g, '""')}"`)
|
|
|
- .join(','),
|
|
|
- );
|
|
|
- const blob = new Blob(['\ufeff' + [header.join(','), ...lines].join('\n')], {
|
|
|
- type: 'text/csv;charset=utf-8;',
|
|
|
- });
|
|
|
- const url = URL.createObjectURL(blob);
|
|
|
- const link = document.createElement('a');
|
|
|
- link.href = url;
|
|
|
- link.download = `考勤明细-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
|
- document.body.appendChild(link);
|
|
|
- link.click();
|
|
|
- document.body.removeChild(link);
|
|
|
- URL.revokeObjectURL(url);
|
|
|
+ const body = this.currentRows.map((row, index) => [
|
|
|
+ index + 1,
|
|
|
+ row.name,
|
|
|
+ row.organization,
|
|
|
+ row.department,
|
|
|
+ row.attendanceDate,
|
|
|
+ row.statusText,
|
|
|
+ row.punchTypes,
|
|
|
+ row.punchAddress,
|
|
|
+ row.punchStatuses.map((item) => item.statusText).join(' '),
|
|
|
+ row.actualIn,
|
|
|
+ row.actualOut,
|
|
|
+ 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 book = XLSX.utils.book_new();
|
|
|
+ XLSX.utils.book_append_sheet(book, sheet, '考勤明细');
|
|
|
+ XLSX.writeFile(book, `考勤明细-${new Date().toISOString().slice(0, 10)}.xlsx`);
|
|
|
},
|
|
|
},
|
|
|
};
|