|
|
@@ -76,8 +76,9 @@
|
|
|
:cache-key="$tableCacheKey('hr-attendance-daily-v9')"
|
|
|
>
|
|
|
<template v-slot:toolbar>
|
|
|
- <!-- 后端暂无导入接口:点击如实提示,缺口已转后端改造清单 -->
|
|
|
- <el-button icon="el-icon-upload2" @click="onImport">导入excel</el-button>
|
|
|
+ <!-- 导入模板列与列表一致(必填列带*),导入直接覆盖每日结果、后端不重算 -->
|
|
|
+ <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 type="primary" icon="el-icon-download" @click="onExport">excel导出</el-button>
|
|
|
</template>
|
|
|
|
|
|
@@ -206,6 +207,8 @@
|
|
|
<el-descriptions-item label="夜班(分钟)">{{ minuteText(detailRow.nightMinutes) }}</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="数据来源" :span="2">{{ importSourceText(detailRow) }}</el-descriptions-item>
|
|
|
</el-descriptions>
|
|
|
|
|
|
<div v-if="detailRow" class="punch-records">
|
|
|
@@ -238,6 +241,8 @@ import {
|
|
|
import {
|
|
|
getDailyAttendanceDetailPage,
|
|
|
adaptDailyAttendance,
|
|
|
+ downloadDailyImportTemplate,
|
|
|
+ importDailyAttendance,
|
|
|
} from '@/api/hr/attendance';
|
|
|
|
|
|
/* 日期筛选默认当月:本月 1 号 ~ 今天(2026-10-30 用户指定默认查当月) */
|
|
|
@@ -300,6 +305,7 @@ export default {
|
|
|
currentRows: [],
|
|
|
detailVisible: false,
|
|
|
detailRow: null,
|
|
|
+ importing: false,
|
|
|
};
|
|
|
},
|
|
|
created() {
|
|
|
@@ -396,12 +402,73 @@ export default {
|
|
|
minuteText(value) {
|
|
|
return value === null || value === undefined ? '' : String(value);
|
|
|
},
|
|
|
+ /* 覆盖更新标识:后端返回 importFlag=1 表示该日结果被 Excel 导入覆盖过 */
|
|
|
+ importSourceText(row) {
|
|
|
+ const raw = row?.raw || {};
|
|
|
+ if (Number(raw.importFlag) !== 1) return '系统核算';
|
|
|
+ const time = String(raw.importTime || '').replace('T', ' ').slice(0, 19);
|
|
|
+ const parts = [time && `时间 ${time}`, raw.importBatchNo && `批次 ${raw.importBatchNo}`].filter(Boolean);
|
|
|
+ return parts.length ? `Excel导入覆盖(${parts.join(',')})` : 'Excel导入覆盖';
|
|
|
+ },
|
|
|
viewRow(row) {
|
|
|
this.detailRow = row;
|
|
|
this.detailVisible = true;
|
|
|
},
|
|
|
+ /* 下载导入模板:表头与列表一致,必填列带*(日期/姓名/所属机构/所属部门/出勤情况/工作地址) */
|
|
|
+ async downloadTemplate() {
|
|
|
+ try {
|
|
|
+ await downloadDailyImportTemplate();
|
|
|
+ } catch (error) {
|
|
|
+ this.$message.error(error.message || '模板下载失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /* 导入考勤明细:后端直接覆盖每日结果(含锁定与已调整数据)并打导入标识,导入后不重算 */
|
|
|
onImport() {
|
|
|
- this.$message.info('导入考勤明细的后端接口暂未提供,请后端补充导入接口');
|
|
|
+ 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();
|
|
|
},
|
|
|
/* 后端暂无导出接口:先导出当前页已加载数据(xlsx,SheetJS 本地生成,与员工档案导入模板同款);
|
|
|
全量导出待后端补接口。注意 SheetJS 社区版只支持写 .xlsx,写不了老格式 .xls(BIFF8)。
|