Prechádzať zdrojové kódy

feat: hr新增考勤明细Excel导入覆盖每日考勤结果与模板示例行

lucw 12 hodín pred
rodič
commit
70915e41c9

+ 27 - 0
src/api/hr/attendance.js

@@ -1,4 +1,5 @@
 import request from '@/utils/request';
+import { downloadBlobResponse } from '@/utils/download';
 
 function unwrap(response) {
   const result = response?.data || {};
@@ -1961,6 +1962,32 @@ function formatDailyTime(value) {
   return match[2] !== undefined ? `${match[1]}:${match[2]}` : `${match[1]}:00`;
 }
 
+/**
+ * 下载考勤明细导入模板
+ * GET /hr/attendance/daily/import/template
+ */
+export async function downloadDailyImportTemplate() {
+  await downloadBlobResponse(
+    await request.get('/hr/attendance/daily/import/template', { responseType: 'blob' }),
+    '考勤明细导入模板.xlsx',
+  );
+}
+
+/**
+ * 导入考勤明细 Excel(直接覆盖每日考勤结果,不触发重算)
+ * POST /hr/attendance/daily/import/excel
+ * 返回 { totalCount, successCount, failedCount, errors: [{ rowNumber, employeeCode, employeeName, message }] }
+ */
+export async function importDailyAttendance(file) {
+  const form = new FormData();
+  form.append('file', file);
+  return unwrap(
+    await request.post('/hr/attendance/daily/import/excel', form, {
+      headers: { 'Content-Type': 'multipart/form-data' },
+    }),
+  );
+}
+
 /**
  * 分页查询每日考勤(夜班归属班次开始日)
  * GET /hr/attendance/daily

+ 70 - 3
src/views/attendance/daily/index.vue

@@ -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)。