<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-for="(item) in tableData"
|
:key="item.id"
|
class="record-item-card"
|
:class="{ 'abnormal': item.status !== 'normal' }">
|
<view class="record-item-header">
|
<text class="record-date">{{ item.date }}</text>
|
<u-tag :type="item.status === 'normal' ? 'success' : 'error'"
|
size="small">
|
{{ item.statusText }}
|
</u-tag>
|
</view>
|
<view class="record-item-body">
|
<view class="record-detail">
|
<text class="detail-label">员工</text>
|
<text class="detail-value">{{ item.name }} ({{ item.no }})</text>
|
</view>
|
<view class="record-detail">
|
<text class="detail-label">部门</text>
|
<text class="detail-value">{{ item.dept }}</text>
|
</view>
|
<view class="record-detail">
|
<text class="detail-label">上班时间</text>
|
<text class="detail-value">{{ item.checkInTime ? item.checkInTime : '缺卡' }}</text>
|
</view>
|
<view class="record-detail">
|
<text class="detail-label">下班时间</text>
|
<text class="detail-value">{{ item.checkOutTime? item.checkOutTime : '缺卡' }}</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"
|
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";
|
|
// 模拟当前登录员工
|
const currentUser = reactive({
|
id: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
});
|
|
// 模拟考勤原始数据
|
const rawAttendance = ref([
|
{
|
id: 1,
|
date: "2026-02-09",
|
userId: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
checkInTime: "08:58",
|
checkOutTime: "",
|
workHours: null,
|
status: "normal",
|
statusText: "正常",
|
remark: "",
|
},
|
{
|
id: 2,
|
date: "2026-02-08",
|
userId: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
checkInTime: "09:15",
|
checkOutTime: "18:05",
|
workHours: 8.8,
|
status: "late",
|
statusText: "迟到",
|
remark: "因交通拥堵迟到",
|
},
|
{
|
id: 3,
|
date: "2026-02-07",
|
userId: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
checkInTime: "08:45",
|
checkOutTime: "18:30",
|
workHours: 9.7,
|
status: "normal",
|
statusText: "正常",
|
remark: "加班0.5小时",
|
},
|
{
|
id: 4,
|
date: "2026-02-06",
|
userId: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
checkInTime: "08:50",
|
checkOutTime: "17:45",
|
workHours: 8.9,
|
status: "early",
|
statusText: "早退",
|
remark: "家中有事提前离开",
|
},
|
{
|
id: 5,
|
date: "2026-02-05",
|
userId: 1,
|
name: "张三",
|
no: "E10001",
|
dept: "生产一部",
|
checkInTime: "08:40",
|
checkOutTime: "18:20",
|
workHours: 9.7,
|
status: "normal",
|
statusText: "正常",
|
remark: "加班0.5小时",
|
},
|
]);
|
|
// 查询表单
|
const searchForm = reactive({
|
date: "",
|
});
|
|
// 表格数据
|
const tableData = ref([]);
|
|
// 返回上一页
|
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 selectDate = () => {
|
showDatePicker.value = true;
|
};
|
|
// 清除日期选择
|
const clearDate = () => {
|
searchForm.date = "";
|
handleQuery();
|
};
|
|
// 查询
|
const recomputeTable = () => {
|
const list = rawAttendance.value.filter(item => {
|
if (searchForm.date && item.date !== searchForm.date) {
|
return false;
|
}
|
return true;
|
});
|
tableData.value = list;
|
};
|
|
const handleQuery = () => {
|
recomputeTable();
|
};
|
|
onMounted(() => {
|
recomputeTable();
|
});
|
</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;
|
}
|
|
.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>
|