<template>
|
<view class="attendance-report">
|
<!-- 页面头部 -->
|
<PageHeader title="考勤日报"
|
@back="goBack" />
|
<!-- 搜索和筛选区域 -->
|
<view class="search-section">
|
<view class="search-bar">
|
<view @click="selectDate"
|
class="search-input">
|
<view class="search-text">{{ searchForm.date? searchForm.date : '请选择日期' }}</view>
|
</view>
|
<view class="filter-button"
|
@click="clearDate">
|
<u-icon name="close-circle"
|
size="24"
|
color="#999"></u-icon>
|
</view>
|
</view>
|
</view>
|
<!-- 日期选择器 -->
|
<up-datetime-picker :show="showDatePicker"
|
mode="date"
|
v-model="currentDate"
|
@confirm="handleDateConfirm"
|
@cancel="showDatePicker = false"
|
title="搜索日期" />
|
<view class="record-list">
|
<!-- 加载状态 -->
|
<view v-if="loading"
|
class="loading-state">
|
<u-icon name="loading"
|
size="40"
|
color="#348fe2"></u-icon>
|
<text class="loading-text">加载中...</text>
|
</view>
|
<view v-else
|
v-for="(item) in tableData"
|
:key="item.id"
|
class="record-item-card"
|
:class="{ 'abnormal': item.status !== 0 }">
|
<view class="record-item-header">
|
<text class="record-date">{{ item.date }}</text>
|
<u-tag :type="item.status === 0 ? 'success' : 'error'"
|
size="small">
|
<!-- {{ item.status === 0 ? '正常' : (item.status === 1 ? '迟到' : (item.status === 2 ? '早退' : '迟到、早退')) }} -->
|
{{ getStatusText(item.status) }}
|
</u-tag>
|
</view>
|
<view class="record-item-body">
|
<view class="record-detail">
|
<text class="detail-label">员工</text>
|
<text class="detail-value">{{ item.staffName }} ({{ item.staffNo }})</text>
|
</view>
|
<view class="record-detail">
|
<text class="detail-label">部门</text>
|
<text class="detail-value">{{ item.deptName }}</text>
|
</view>
|
<view class="record-detail">
|
<text class="detail-label">上班时间</text>
|
<text class="detail-value">{{ item.workStartAt || '缺卡' }}</text>
|
</view>
|
<view class="record-detail">
|
<text class="detail-label">下班时间</text>
|
<text class="detail-value">{{ item.workEndAt || '缺卡' }}</text>
|
</view>
|
<view class="record-detail">
|
<text class="detail-label">工时</text>
|
<text class="detail-value">{{ item.workHours ? item.workHours + '小时' : '-' }}</text>
|
</view>
|
<view v-if="item.remark"
|
class="record-detail">
|
<text class="detail-label">备注</text>
|
<text class="detail-value">{{ item.remark }}</text>
|
</view>
|
</view>
|
</view>
|
<!-- 空状态 -->
|
<view v-if="tableData.length === 0 && !loading"
|
class="empty-state">
|
<!-- <u-icon name="clock-o"
|
size="60"
|
color="#999"></u-icon> -->
|
<text class="empty-text">暂无考勤记录</text>
|
</view>
|
</view>
|
<!-- 导出按钮 -->
|
<!-- <view class="export-section">
|
<u-button type="default"
|
size="medium"
|
text="导出考勤日报"
|
@click="handleExport"
|
class="export-btn"></u-button>
|
</view> -->
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted } from "vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
import dayjs from "dayjs";
|
import { findPersonalAttendanceRecords } from "@/api/personnelManagement/attendance.js";
|
|
// 查询表单
|
const searchForm = reactive({
|
date: "",
|
});
|
|
// 分页参数
|
const page = reactive({
|
current: -1,
|
size: -1,
|
total: 0,
|
});
|
|
// 表格数据
|
const tableData = ref([]);
|
|
// 加载状态
|
const loading = ref(false);
|
|
// 返回上一页
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
|
// 日期选择器
|
const showDatePicker = ref(false);
|
const currentDate = ref(new Date());
|
|
// 处理日期选择
|
const handleDateConfirm = e => {
|
currentDate.value = e.value;
|
searchForm.date = dayjs(e.value).format("YYYY-MM-DD");
|
showDatePicker.value = false;
|
handleQuery();
|
};
|
const getStatusText = status => {
|
switch (status) {
|
case 0:
|
return "正常";
|
case 1:
|
return "迟到";
|
case 2:
|
return "早退";
|
case 3:
|
return "迟到、早退";
|
case 4:
|
return "缺勤";
|
}
|
};
|
|
// 显示日期选择器
|
const selectDate = () => {
|
showDatePicker.value = true;
|
};
|
|
// 清除日期选择
|
const clearDate = () => {
|
resetQuery();
|
};
|
|
// 查询
|
const handleQuery = () => {
|
loading.value = true;
|
console.log(searchForm, "searchForm");
|
|
findPersonalAttendanceRecords({
|
...page,
|
...searchForm,
|
})
|
.then(res => {
|
tableData.value = res.data.records;
|
page.total = res.data.total;
|
})
|
.catch(error => {
|
console.error("查询失败:", error);
|
uni.showToast({
|
title: "查询失败,请重试",
|
icon: "none",
|
});
|
})
|
.finally(() => {
|
loading.value = false;
|
});
|
};
|
|
// 重置查询
|
const resetQuery = () => {
|
searchForm.date = "";
|
handleQuery();
|
};
|
|
onMounted(() => {
|
handleQuery();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
// 全局变量
|
$primary-color: #2c7be5;
|
$primary-light: #4a90e2;
|
$success-color: #4cd964;
|
$warning-color: #ff9500;
|
$danger-color: #ff3b30;
|
$text-primary: #333333;
|
$text-secondary: #666666;
|
$text-tertiary: #999999;
|
$bg-color: #f5f7fa;
|
$card-bg: #ffffff;
|
$border-color: #e8e8e8;
|
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
$shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
|
$shadow-lg: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
|
.attendance-report {
|
min-height: 100vh;
|
background-color: $bg-color;
|
padding-bottom: 30rpx;
|
background-image: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);
|
}
|
|
/* 搜索和筛选区域 */
|
.search-section {
|
background-color: $card-bg;
|
margin: 20rpx;
|
border-radius: 16rpx;
|
box-shadow: $shadow-md;
|
padding: 20rpx;
|
margin-bottom: 24rpx;
|
transition: all 0.3s ease;
|
}
|
|
.search-section:hover {
|
box-shadow: $shadow-lg;
|
transform: translateY(-2rpx);
|
}
|
|
.search-bar {
|
display: flex;
|
align-items: center;
|
background-color: rgba($primary-color, 0.05);
|
border-radius: 8rpx;
|
padding: 0 16rpx;
|
height: 70rpx;
|
}
|
|
.search-input {
|
flex: 1;
|
height: 100%;
|
display: flex;
|
align-items: center;
|
}
|
|
.search-text {
|
font-size: 14px;
|
color: $text-tertiary;
|
height: 70rpx;
|
line-height: 70rpx;
|
margin-left: 8rpx;
|
}
|
|
.filter-button {
|
padding: 8rpx;
|
transition: all 0.3s ease;
|
}
|
|
.filter-button:hover {
|
background-color: rgba($primary-color, 0.1);
|
border-radius: 4rpx;
|
}
|
|
/* 记录列表 */
|
.record-list {
|
margin: 0 20rpx 24rpx;
|
}
|
|
/* 加载状态 */
|
.loading-state {
|
background-color: $card-bg;
|
border-radius: 16rpx;
|
box-shadow: $shadow-md;
|
text-align: center;
|
padding: 120rpx 0;
|
margin: 0 20rpx;
|
transition: all 0.3s ease;
|
}
|
|
.loading-text {
|
font-size: 14px;
|
color: $text-tertiary;
|
margin-top: 24rpx;
|
font-weight: 500;
|
}
|
|
.record-item-card {
|
background-color: $card-bg;
|
border-radius: 16rpx;
|
box-shadow: $shadow-md;
|
margin-bottom: 24rpx;
|
overflow: hidden;
|
transition: all 0.3s ease;
|
}
|
|
.record-item-card:hover {
|
box-shadow: $shadow-lg;
|
transform: translateY(-2rpx);
|
}
|
|
.record-item-card.abnormal {
|
background-color: rgba($danger-color, 0.05);
|
border-left: 4rpx solid $danger-color;
|
}
|
|
.record-item-header {
|
background-color: rgba($primary-color, 0.05);
|
padding: 20rpx;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
border-bottom: 1rpx solid $border-color;
|
}
|
|
.record-date {
|
font-size: 14px;
|
font-weight: 600;
|
color: $text-primary;
|
}
|
|
.record-item-body {
|
padding: 24rpx;
|
}
|
|
.record-detail {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 16rpx;
|
padding: 8rpx 0;
|
border-bottom: 1rpx solid rgba($border-color, 0.5);
|
}
|
|
.record-detail:last-child {
|
margin-bottom: 0;
|
border-bottom: none;
|
}
|
|
.detail-label {
|
font-size: 13px;
|
color: $text-secondary;
|
font-weight: 500;
|
}
|
|
.detail-value {
|
font-size: 13px;
|
color: $text-primary;
|
font-weight: 500;
|
}
|
|
/* 空状态 */
|
.empty-state {
|
background-color: $card-bg;
|
border-radius: 16rpx;
|
box-shadow: $shadow-md;
|
text-align: center;
|
padding: 120rpx 0;
|
margin: 0 20rpx;
|
transition: all 0.3s ease;
|
}
|
|
.empty-state:hover {
|
box-shadow: $shadow-lg;
|
}
|
|
.empty-text {
|
font-size: 14px;
|
color: $text-tertiary;
|
margin-top: 24rpx;
|
font-weight: 500;
|
}
|
|
/* 响应式调整 */
|
@media (max-width: 375px) {
|
.search-section,
|
.record-list,
|
.empty-state {
|
margin: 12rpx;
|
}
|
|
.search-section {
|
padding: 16rpx;
|
}
|
|
.record-item-body {
|
padding: 20rpx;
|
}
|
}
|
|
/* 动画效果 */
|
@keyframes fadeInUp {
|
from {
|
opacity: 0;
|
transform: translateY(20rpx);
|
}
|
to {
|
opacity: 1;
|
transform: translateY(0);
|
}
|
}
|
|
.search-section,
|
.record-item-card,
|
.empty-state {
|
animation: fadeInUp 0.5s ease-out;
|
}
|
|
.record-item-card {
|
animation-delay: 0.1s;
|
}
|
|
.record-item-card:nth-child(2) {
|
animation-delay: 0.2s;
|
}
|
|
.record-item-card:nth-child(3) {
|
animation-delay: 0.3s;
|
}
|
|
.empty-state {
|
animation-delay: 0.2s;
|
}
|
</style>
|