huminmin
2026-02-10 d264fc8d172da088aa71ce2d1e94b21ddb75d662
src/views/personnelManagement/attendanceCheckin/index.vue
@@ -12,20 +12,20 @@
            <div class="label">当前时间</div>
            <div class="value">{{ nowTime }}</div>
          </div>
          <el-button type="primary" size="large" @click="handleCheckInOut">
          <el-button type="primary" size="large" @click="handleCheckInOut" :disabled="todayRecord.workEndAt">
            {{ checkInOutText }}
          </el-button>
        </div>
      </div>
      <el-descriptions border :column="4" class="mt10">
        <el-descriptions-item label="员工姓名">
          {{ currentUser.name }}
          {{ todayRecord.staffName }}
        </el-descriptions-item>
        <el-descriptions-item label="工号">
          {{ currentUser.no }}
          {{ todayRecord.staffNo }}
        </el-descriptions-item>
        <el-descriptions-item label="所属部门">
          {{ currentUser.dept }}
          {{ todayRecord.deptName }}
        </el-descriptions-item>
        <el-descriptions-item label="今日状态">
          <el-tag :type="todayStatusTag" size="small">
@@ -33,10 +33,10 @@
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="上班时间">
          {{ todayRecord?.checkInTime || '-' }}
          {{ todayRecord?.workStartAt || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="下班时间">
          {{ todayRecord?.checkOutTime || '-' }}
          {{ todayRecord?.workEndAt || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="工时(小时)">
          {{ todayRecord?.workHours ?? '-' }}
@@ -50,44 +50,47 @@
      </el-descriptions>
    </el-card>
    <!-- 查询条件(管理员考勤日报) -->
    <div class="search_form">
      <div>
        <span class="search_title">部门:</span>
        <el-select
          v-model="searchForm.dept"
          placeholder="请选择部门"
          style="width: 180px"
          clearable
        >
          <el-option
            v-for="item in deptOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
    <div class="attendance-operation">
      <!-- 查询条件(管理员考勤日报) -->
      <el-form :model="searchForm" :inline="true" class="search-form">
        <el-form-item label="部门:" prop="deptId">
          <el-tree-select
              v-model="searchForm.deptId"
              :data="deptOptions"
              :props="{ value: 'id', label: 'label', children: 'children' }"
              value-key="id"
              placeholder="请选择部门"
              check-strictly
              style="width: 200px"
          />
        </el-select>
        </el-form-item>
        <span class="search_title ml10">日期:</span>
        <el-date-picker
          v-model="searchForm.date"
          type="date"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          placeholder="请选择日期"
          clearable
        />
        <el-form-item label="日期:" prop="date">
          <el-date-picker
              v-model="searchForm.date"
              type="date"
              value-format="YYYY-MM-DD"
              format="YYYY-MM-DD"
              placeholder="请选择日期"
              clearable
          />
        </el-form-item>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          搜索
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button icon="Download" @click="handleExport">
          导出考勤日报
        </el-button>
      </div>
        <el-form-item>
          <el-button type="primary" @click="fetchData">
            <el-icon><Search /></el-icon>
            搜索
          </el-button>
          <el-button @click="resetSearch">
            <el-icon><Refresh /></el-icon>
            重置
          </el-button>
        </el-form-item>
      </el-form>
      <el-button icon="Download" @click="handleExport">
        导出考勤日报
      </el-button>
    </div>
    <!-- 考勤日报表格 -->
@@ -95,6 +98,7 @@
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        style="width: 100%"
        height="calc(100vh - 24em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
@@ -107,45 +111,43 @@
          width="120"
        />
        <el-table-column
          prop="dept"
          prop="deptName"
          label="部门"
          width="140"
        />
        <el-table-column
          prop="name"
          prop="staffName"
          label="姓名"
          width="120"
        />
        <el-table-column
          prop="no"
          prop="staffNo"
          label="工号"
          width="120"
        />
        <el-table-column
          prop="checkInTime"
          prop="workStartAt"
          label="上班时间"
          width="140"
        />
        <el-table-column
          prop="checkOutTime"
          prop="workEndAt"
          label="下班时间"
          width="140"
        />
        <el-table-column
          prop="workHours"
          label="工时(小时)"
          width="110"
          align="center"
        />
        <el-table-column
          prop="statusText"
          prop="status"
          label="考勤状态"
          width="120"
          align="center"
        >
          <template #default="scope">
            <el-tag
              v-if="scope.row.status === 'normal'"
              v-if="scope.row.status === 0"
              type="success"
              size="small"
            >
@@ -156,7 +158,7 @@
              type="danger"
              size="small"
            >
              {{ scope.row.statusText }}
              {{ scope.row.status === 1 ? '迟到' : '早退' }}
            </el-tag>
          </template>
        </el-table-column>
@@ -166,93 +168,41 @@
          show-overflow-tooltip
        />
      </el-table>
      <pagination :total="total" layout="total, sizes, prev, pager, next, jumper"
                  :page="page.current" :limit="page.size" @pagination="paginationChange" />
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
import { ElMessage } from "element-plus";
import {ElMessage, ElMessageBox} from "element-plus";
import {
  createPersonalAttendanceRecord,
  findPersonalAttendanceRecords, findTodayPersonalAttendanceRecord
} from "@/api/personnelManagement/personalAttendanceRecords.js";
import Pagination from "@/components/Pagination/index.vue";
import {deptTreeSelect} from "@/api/system/user.js";
import {Refresh, Search} from "@element-plus/icons-vue";
// 模拟当前登录员工
const currentUser = reactive({
  id: 1,
  name: "张三",
  no: "E10001",
  dept: "生产一部",
});
const { proxy } = getCurrentInstance()
const tableLoading = ref(false)
// 分页参数
const page = reactive({
  current: 1,
  size: 10,
  total: 0
})
// 今日数据
const todayRecord = ref({})
// 部门选项
const deptOptions = [
  { label: "生产一部", value: "生产一部" },
  { label: "生产二部", value: "生产二部" },
  { label: "设备维护部", value: "设备维护部" },
  { label: "质检部", value: "质检部" },
];
// 模拟考勤原始数据
const rawAttendance = ref([
  {
    id: 1,
    date: "2024-12-01",
    userId: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
    checkInTime: "08:58",
    checkOutTime: "18:10",
    workHours: 9.2,
    status: "normal",
    statusText: "正常",
    remark: "",
  },
  {
    id: 2,
    date: "2024-12-01",
    userId: 2,
    name: "李四",
    no: "E10002",
    dept: "生产一部",
    checkInTime: "09:15",
    checkOutTime: "18:05",
    workHours: 8.8,
    status: "late",
    statusText: "迟到",
    remark: "因交通拥堵迟到",
  },
  {
    id: 3,
    date: "2024-12-01",
    userId: 3,
    name: "王五",
    no: "E20001",
    dept: "设备维护部",
    checkInTime: "08:50",
    checkOutTime: "17:20",
    workHours: 8.5,
    status: "early",
    statusText: "早退",
    remark: "外出处理紧急故障",
  },
  {
    id: 4,
    date: "2024-12-02",
    userId: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
    checkInTime: "08:45",
    checkOutTime: "18:30",
    workHours: 9.7,
    status: "normal",
    statusText: "正常",
    remark: "加班0.5小时",
  },
]);
const deptOptions = ref([])
// 查询表单
const searchForm = reactive({
  dept: "",
  deptId: "",
  date: "",
});
@@ -274,23 +224,12 @@
  nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
};
// 今日日期
const todayStr = computed(() => nowTime.value.slice(0, 10));
// 当日当前员工考勤记录
const todayRecord = computed(() =>
  rawAttendance.value.find(
    (item) =>
      item.userId === currentUser.id && item.date === todayStr.value
  )
);
// 打卡按钮文本
const checkInOutText = computed(() => {
  if (!todayRecord.value || !todayRecord.value.checkInTime) {
  if (!todayRecord.value || !todayRecord.value.workStartAt) {
    return "上班打卡";
  }
  if (!todayRecord.value.checkOutTime) {
  if (!todayRecord.value.workEndAt) {
    return "下班打卡";
  }
  return "今日已打卡完成";
@@ -298,106 +237,103 @@
// 今日状态展示
const todayStatusTag = computed(() => {
  if (!todayRecord.value) return "info";
  if (todayRecord.value.status === "normal") return "success";
  if (!todayRecord.value.id) return "info";
  if (todayRecord.value.status === 0) return "success";
  return "danger";
});
const todayStatusText = computed(() => {
  if (!todayRecord.value) return "未打卡";
  return todayRecord.value.statusText || "正常";
  if (!todayRecord.value.id) return "未打卡";
  switch (todayRecord.value.status) {
    case 0:
      return "正常"
    case 1:
      return "迟到"
    case 2:
      return "早退"
  }
});
// 行样式:异常高亮
const rowClassName = ({ row }) => {
  if (row.status === "late" || row.status === "early") {
  if (row.status === 1 || row.status === 2) {
    return "row-abnormal";
  }
  return "";
};
// 查询部门列表
const fetchDeptOptions = () => {
  deptTreeSelect().then(response => {
    deptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
  })
}
/** 过滤禁用的部门 */
function filterDisabledDept(deptList) {
  return deptList.filter(dept => {
    if (dept.disabled) {
      return false
    }
    if (dept.children && dept.children.length) {
      dept.children = filterDisabledDept(dept.children)
    }
    return true
  })
}
// 查询
const recomputeTable = () => {
  const list = rawAttendance.value.filter((item) => {
    if (searchForm.dept && item.dept !== searchForm.dept) {
      return false;
    }
    if (searchForm.date && item.date !== searchForm.date) {
      return false;
    }
    return true;
const fetchData = () => {
  tableLoading.value = true
  findPersonalAttendanceRecords({...page, ...searchForm}).then((res) => {
    tableData.value = res.data.records;
    page.value.total = res.data.total;
  }).finally(() => {
    tableLoading.value = false;
  });
  tableData.value = list;
};
const handleQuery = () => {
  recomputeTable();
// 查询今日打卡信息
const fetchTodayData = () => {
  findTodayPersonalAttendanceRecord({}).then((res) => {
    todayRecord.value = res.data;
  })
};
const paginationChange = (pagination) => {
  page.current = pagination.page;
  page.size = pagination.limit;
  fetchData();
}
const resetSearch = () => {
  searchForm.dept = "";
  searchForm.deptId = "";
  searchForm.date = "";
  recomputeTable();
  fetchData();
};
// 导出(演示)
const handleExport = () => {
  ElMessage.success("当前为演示页面,导出功能未对接实际接口");
  ElMessageBox.confirm("是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
      .then(() => {
        proxy.download("/personalAttendanceRecords/export", {}, "考勤记录.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
};
// 打卡逻辑(仅前端模拟)
// 打卡
const handleCheckInOut = () => {
  const [dateStr, timeStr] = nowTime.value.split(" ");
  if (!dateStr || !timeStr) return;
  // 上班打卡
  if (!todayRecord.value) {
    const newId = rawAttendance.value.length
      ? Math.max(...rawAttendance.value.map((i) => i.id)) + 1
      : 1;
    const status =
      timeStr > "09:00:00" ? "late" : "normal";
    const statusText = status === "late" ? "迟到" : "正常";
    rawAttendance.value.push({
      id: newId,
      date: dateStr,
      userId: currentUser.id,
      name: currentUser.name,
      no: currentUser.no,
      dept: currentUser.dept,
      checkInTime: timeStr.slice(0, 5),
      checkOutTime: "",
      workHours: null,
      status,
      statusText,
      remark: "",
    });
    ElMessage.success("上班打卡成功");
  } else if (!todayRecord.value.checkOutTime) {
    // 下班打卡
    todayRecord.value.checkOutTime = timeStr.slice(0, 5);
    // 简单按 9:00-18:00 计算工时
    const start = todayRecord.value.checkInTime || "09:00";
    const [sh, sm] = start.split(":").map((v) => parseInt(v, 10));
    const [eh, em] = todayRecord.value.checkOutTime
      .split(":")
      .map((v) => parseInt(v, 10));
    const diff = (eh * 60 + em - (sh * 60 + sm)) / 60;
    todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1));
    // 早退判断:18:00 前离开视为早退(只示意)
    if (timeStr < "18:00:00") {
      todayRecord.value.status = "early";
      todayRecord.value.statusText = "早退";
    } else if (todayRecord.value.status === "normal") {
      todayRecord.value.statusText = "正常";
    }
    ElMessage.success("下班打卡成功");
  } else {
    ElMessage.info("今日已完成上下班打卡");
  }
  recomputeTable();
  createPersonalAttendanceRecord({}).then((res) => {
    fetchData()
    fetchTodayData()
    ElMessage.success("打卡成功!");
  })
};
onMounted(() => {
@@ -409,7 +345,9 @@
  const M = String(today.getMonth() + 1).padStart(2, "0");
  const D = String(today.getDate()).padStart(2, "0");
  searchForm.date = `${Y}-${M}-${D}`;
  recomputeTable();
  fetchData();
  fetchTodayData()
  fetchDeptOptions();
});
onBeforeUnmount(() => {
@@ -465,5 +403,10 @@
::v-deep(.row-abnormal) {
  background-color: #fff5f5;
}
.attendance-operation {
  display: flex;
  justify-content: space-between;
}
</style>