|
|
@@ -4,60 +4,24 @@
|
|
|
<!-- 筛选区:所属机构 / 所属部门 / 姓名 / 日期 -->
|
|
|
<el-form inline :model="filter" class="daily-filter" @submit.native.prevent>
|
|
|
<el-form-item label="所属机构">
|
|
|
- <el-select
|
|
|
- v-model="filter.orgId"
|
|
|
- placeholder="请选择"
|
|
|
- clearable
|
|
|
- filterable
|
|
|
- style="width: 220px"
|
|
|
- @change="onOrgChange"
|
|
|
- >
|
|
|
- <el-option
|
|
|
- v-for="item in filterOrgOptions"
|
|
|
- :key="item.value"
|
|
|
- :label="item.label"
|
|
|
- :value="item.value"
|
|
|
- />
|
|
|
+ <el-select v-model="filter.orgId" placeholder="请选择" clearable 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
|
|
|
- v-model="filter.deptId"
|
|
|
- placeholder="全部部门"
|
|
|
- clearable
|
|
|
- filterable
|
|
|
- style="width: 220px"
|
|
|
- @change="search"
|
|
|
- >
|
|
|
- <el-option
|
|
|
- v-for="item in filterDeptOptions"
|
|
|
- :key="item.value"
|
|
|
- :label="item.label"
|
|
|
- :value="item.value"
|
|
|
- />
|
|
|
+ <el-select v-model="filter.deptId" placeholder="全部部门" clearable filterable style="width: 220px"
|
|
|
+ @change="search">
|
|
|
+ <el-option 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-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"
|
|
|
- type="daterange"
|
|
|
- value-format="yyyy-MM-dd"
|
|
|
- range-separator="-"
|
|
|
- start-placeholder="开始日期"
|
|
|
- end-placeholder="结束日期"
|
|
|
- style="width: 260px"
|
|
|
- @change="search"
|
|
|
- />
|
|
|
+ <el-date-picker v-model="filter.dateRange" type="daterange" value-format="yyyy-MM-dd" range-separator="-"
|
|
|
+ start-placeholder="开始日期" end-placeholder="结束日期" style="width: 260px" @change="search" />
|
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
|
<el-button type="primary" icon="el-icon-search" @click="search">查询</el-button>
|
|
|
@@ -65,32 +29,20 @@
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
|
|
|
- <ele-pro-table
|
|
|
- ref="table"
|
|
|
- row-key="id"
|
|
|
- :columns="columns"
|
|
|
- :datasource="fetchPage"
|
|
|
- :need-page="true"
|
|
|
- :page-size="20"
|
|
|
- :page-sizes="[10, 20, 50]"
|
|
|
- :cache-key="$tableCacheKey('hr-attendance-daily-v9')"
|
|
|
- >
|
|
|
+ <ele-pro-table ref="table" row-key="id" :columns="columns" :datasource="fetchPage" :need-page="true"
|
|
|
+ :page-size="20" :page-sizes="[10, 20, 50]" :cache-key="$tableCacheKey('hr-attendance-daily-v9')">
|
|
|
<template v-slot:toolbar>
|
|
|
- <!-- 导入模板列与列表一致(必填列带*),导入直接覆盖每日结果、后端不重算 -->
|
|
|
- <el-button icon="el-icon-document" @click="downloadTemplate">下载模板</el-button>
|
|
|
- <el-button icon="el-icon-upload2" :loading="importing" @click="onImport">导入excel</el-button>
|
|
|
+ <el-button icon="el-icon-upload2" @click="onImport">导入excel</el-button>
|
|
|
<el-button type="primary" icon="el-icon-download" @click="onExport">excel导出</el-button>
|
|
|
</template>
|
|
|
|
|
|
<template slot="name" slot-scope="{ row }">
|
|
|
- <!-- 点姓名看详情(替代原操作列「查看」入口) -->
|
|
|
<el-link type="primary" :underline="false" @click="viewRow(row)">
|
|
|
{{ row.name || '—' }}
|
|
|
</el-link>
|
|
|
</template>
|
|
|
|
|
|
<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 }}
|
|
|
@@ -99,72 +51,54 @@
|
|
|
</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"
|
|
|
- >
|
|
|
+ <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 slot="outStatus" slot-scope="{ row }">
|
|
|
- <!-- 下班·打卡情况:punchRecords 中 punchType=OUT 记录的 statusName -->
|
|
|
<template v-if="row.outPunches.length">
|
|
|
- <el-tag
|
|
|
- v-for="(item, index) in row.outPunches"
|
|
|
- :key="index"
|
|
|
- size="mini"
|
|
|
- :type="punchStatusTagType(item)"
|
|
|
- class="punch-tag"
|
|
|
- >
|
|
|
+ <el-tag v-for="(item, index) in row.outPunches" :key="index" size="mini" :type="punchStatusTagType(item)"
|
|
|
+ class="punch-tag">
|
|
|
{{ item.statusText }}
|
|
|
</el-tag>
|
|
|
</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>
|
|
|
@@ -176,13 +110,10 @@
|
|
|
</ele-pro-table>
|
|
|
</el-card>
|
|
|
|
|
|
+ <attendance-import-dialog ref="importDialog" @success="search" />
|
|
|
+
|
|
|
<!-- 查看详情弹框 -->
|
|
|
- <ele-modal
|
|
|
- title="考勤明细详情"
|
|
|
- :visible.sync="detailVisible"
|
|
|
- width="720px"
|
|
|
- append-to-body
|
|
|
- >
|
|
|
+ <ele-modal title="考勤明细详情" :visible.sync="detailVisible" width="720px" append-to-body>
|
|
|
<el-descriptions v-if="detailRow" :column="2" border size="small">
|
|
|
<el-descriptions-item label="姓名">{{ detailRow.name }}</el-descriptions-item>
|
|
|
<el-descriptions-item label="岗位">{{ detailRow.postName }}</el-descriptions-item>
|
|
|
@@ -241,9 +172,8 @@ import {
|
|
|
import {
|
|
|
getDailyAttendanceDetailPage,
|
|
|
adaptDailyAttendance,
|
|
|
- downloadDailyImportTemplate,
|
|
|
- importDailyAttendance,
|
|
|
} from '@/api/hr/attendance';
|
|
|
+import AttendanceImportDialog from './components/AttendanceImportDialog.vue';
|
|
|
|
|
|
/* 日期筛选默认当月:本月 1 号 ~ 今天(2026-10-30 用户指定默认查当月) */
|
|
|
function currentMonthRange() {
|
|
|
@@ -256,6 +186,9 @@ function currentMonthRange() {
|
|
|
|
|
|
export default {
|
|
|
name: 'AttendanceDaily',
|
|
|
+ components: {
|
|
|
+ AttendanceImportDialog,
|
|
|
+ },
|
|
|
data() {
|
|
|
return {
|
|
|
filter: {
|
|
|
@@ -270,13 +203,11 @@ export default {
|
|
|
orgTreeNodes: [],
|
|
|
columns: [
|
|
|
{ type: 'index', label: '序号', width: 60, align: 'center', headerAlign: 'center', fixed: 'left' },
|
|
|
- /* 日期列不排序:后端 /hr/attendance/daily 无排序参数,服务端分页下前端排序只能排当前页,会误导 */
|
|
|
{ prop: 'attendanceDate', label: '日期', minWidth: 110, fixed: 'left' },
|
|
|
{ prop: 'name', label: '姓名', minWidth: 100, slot: 'name', fixed: 'left' },
|
|
|
{ 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: '上班',
|
|
|
@@ -284,9 +215,8 @@ export default {
|
|
|
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' },
|
|
|
+ { prop: 'inTime', label: '打卡时间', minWidth: 90, slot: 'inTime' },
|
|
|
+ { prop: 'inAddress', label: '打卡地点', minWidth: 90, slot: 'inAddress' },
|
|
|
],
|
|
|
},
|
|
|
{
|
|
|
@@ -296,16 +226,14 @@ export default {
|
|
|
{ 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: 'outAddress', label: '打卡地点', minWidth: 90, slot: 'outAddress' },
|
|
|
],
|
|
|
},
|
|
|
{ prop: 'workAddress', label: '工作地址', minWidth: 140 },
|
|
|
],
|
|
|
- /* 当前页数据快照:本地 excel 导出用(后端暂无导出接口) */
|
|
|
currentRows: [],
|
|
|
detailVisible: false,
|
|
|
detailRow: null,
|
|
|
- importing: false,
|
|
|
};
|
|
|
},
|
|
|
created() {
|
|
|
@@ -414,61 +342,10 @@ export default {
|
|
|
this.detailRow = row;
|
|
|
this.detailVisible = true;
|
|
|
},
|
|
|
- /* 下载导入模板:表头与列表一致,必填列带*(日期/姓名/所属机构/所属部门/出勤情况/工作地址) */
|
|
|
- async downloadTemplate() {
|
|
|
- try {
|
|
|
- await downloadDailyImportTemplate();
|
|
|
- } catch (error) {
|
|
|
- this.$message.error(error.message || '模板下载失败');
|
|
|
- }
|
|
|
- },
|
|
|
- /* 导入考勤明细:后端直接覆盖每日结果(含锁定与已调整数据)并打导入标识,导入后不重算 */
|
|
|
+ /* 导入考勤明细:后端直接覆盖每日结果(含锁定与已调整数据)并打导入标识,导入后不重算。
|
|
|
+ 上传/模板下载/结果反馈都在 AttendanceImportDialog 内完成 */
|
|
|
onImport() {
|
|
|
- const input = document.createElement('input');
|
|
|
- input.type = 'file';
|
|
|
- input.accept = '.xlsx,.xls';
|
|
|
- const cleanup = () => {
|
|
|
- if (input.parentNode) input.parentNode.removeChild(input);
|
|
|
- };
|
|
|
- //挂到 body 再点击,兼容部分浏览器对游离 input 的 click 限制
|
|
|
- document.body.appendChild(input);
|
|
|
- input.onchange = async () => {
|
|
|
- const file = input.files?.[0];
|
|
|
- if (!file) {
|
|
|
- cleanup();
|
|
|
- return;
|
|
|
- }
|
|
|
- this.importing = true;
|
|
|
- try {
|
|
|
- const result = await importDailyAttendance(file);
|
|
|
- const errors = (result.errors || [])
|
|
|
- .slice(0, 3)
|
|
|
- .map(
|
|
|
- (item) =>
|
|
|
- `第${item.rowNumber}行${item.employeeName ? `(${item.employeeName})` : ''}:${
|
|
|
- item.message || '导入失败'
|
|
|
- }`,
|
|
|
- )
|
|
|
- .join(';');
|
|
|
- const warnings = (result.warnings || []).slice(0, 3).join(';');
|
|
|
- this.$alert(
|
|
|
- `总计 ${result.totalCount || 0} 条,成功 ${result.successCount || 0} 条,失败 ${
|
|
|
- result.failedCount || 0
|
|
|
- } 条${errors ? `;${errors}` : ''}${warnings ? `;提示:${warnings}` : ''}`,
|
|
|
- '考勤明细导入结果',
|
|
|
- { type: result.failedCount || warnings ? 'warning' : 'success' },
|
|
|
- );
|
|
|
- this.search();
|
|
|
- } catch (error) {
|
|
|
- this.$message.error(error.message || '考勤明细导入失败');
|
|
|
- } finally {
|
|
|
- this.importing = false;
|
|
|
- cleanup();
|
|
|
- }
|
|
|
- };
|
|
|
- //用户取消选择时 onchange 不触发,避免 DOM 上残留隐藏 input
|
|
|
- input.oncancel = cleanup;
|
|
|
- input.click();
|
|
|
+ this.$refs.importDialog.open();
|
|
|
},
|
|
|
/* 后端暂无导出接口:先导出当前页已加载数据(xlsx,SheetJS 本地生成,与员工档案导入模板同款);
|
|
|
全量导出待后端补接口。注意 SheetJS 社区版只支持写 .xlsx,写不了老格式 .xls(BIFF8)。
|
|
|
@@ -532,21 +409,26 @@ export default {
|
|
|
margin-bottom: 4px;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
.punch-tag {
|
|
|
margin-right: 6px;
|
|
|
margin-bottom: 2px;
|
|
|
}
|
|
|
+
|
|
|
/* 上下班分组「打卡时间」列:多条打卡记录逐行展示 */
|
|
|
.punch-time-line {
|
|
|
line-height: 20px;
|
|
|
}
|
|
|
+
|
|
|
/* 出勤情况列:tag 一行一个、强制左对齐(防全局/表头样式带偏) */
|
|
|
.status-tag-line {
|
|
|
text-align: left;
|
|
|
line-height: 26px;
|
|
|
}
|
|
|
+
|
|
|
.punch-records {
|
|
|
margin-top: 16px;
|
|
|
+
|
|
|
h4 {
|
|
|
margin: 0 0 8px;
|
|
|
font-size: 14px;
|