418 lines
10 KiB
Vue
418 lines
10 KiB
Vue
<template>
|
||
<view class="page-container">
|
||
<!-- 导航栏 -->
|
||
<NavBarModal navBarTitle="考勤统计">
|
||
<template #left>
|
||
<view class="back-btn" @tap="handleBack">
|
||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||
</view>
|
||
</template>
|
||
</NavBarModal>
|
||
|
||
<!-- 内容区域 -->
|
||
<view class="content-wrapper" :style="contentStyle">
|
||
<!-- 日历区域 -->
|
||
<view class="calendar-section">
|
||
<!-- 月份导航 -->
|
||
<view class="month-header">
|
||
<view class="month-nav-btn" @tap="handlePrevMonth">
|
||
<up-icon name="arrow-left" size="20" color="#333" />
|
||
</view>
|
||
<text class="month-text">{{ currentMonthText }}</text>
|
||
<view class="month-nav-btn" @tap="handleNextMonth">
|
||
<up-icon name="arrow-right" size="20" color="#333" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 星期标题 -->
|
||
<view class="weekdays-header">
|
||
<view v-for="(day, index) in weekdays" :key="index" class="weekday-cell">
|
||
<text class="weekday-text">{{ day }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日历网格 -->
|
||
<view class="calendar-grid">
|
||
<view
|
||
v-for="(date, index) in calendarDays"
|
||
:key="index"
|
||
class="date-cell"
|
||
:class="{
|
||
'other-month': !date.isCurrentMonth,
|
||
selected: date.isSelected,
|
||
'has-abnormal': date.hasAbnormal,
|
||
}"
|
||
@tap="handleDateClick(date)"
|
||
>
|
||
<text class="date-number">{{ date.day }}</text>
|
||
<text v-if="date.hasAbnormal" class="abnormal-label">异常</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日期详情区域 -->
|
||
<view class="detail-section">
|
||
<text class="detail-date">{{ selectedDateText }}</text>
|
||
<view class="detail-content">
|
||
<view class="detail-line"></view>
|
||
<text class="detail-status">{{ selectedDateStatus }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||
import { getContentStyle } from '@/utils/safeArea'
|
||
import dayjs from 'dayjs'
|
||
|
||
/**
|
||
* 自有考勤统计页面
|
||
* 业务背景:用于查看自有人员的考勤统计数据
|
||
* 设计决策:
|
||
* 1. 使用日历视图展示考勤数据
|
||
* 2. 支持月份切换
|
||
* 3. 标记异常日期
|
||
* 4. 显示选中日期的考勤详情
|
||
*/
|
||
|
||
const contentStyle = computed(() => {
|
||
return getContentStyle({
|
||
includeNavBar: true,
|
||
includeStatusBar: true,
|
||
includeBottomSafeArea: true,
|
||
})
|
||
})
|
||
|
||
// 星期标题
|
||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||
|
||
// 当前显示的月份
|
||
const currentMonth = ref(dayjs())
|
||
|
||
// 选中的日期
|
||
const selectedDate = ref(dayjs())
|
||
|
||
// 异常日期列表(示例数据,后续从接口获取)
|
||
const abnormalDates = ref([
|
||
'2025-11-30',
|
||
'2025-12-01',
|
||
// 可以添加更多异常日期
|
||
])
|
||
|
||
// 日期状态映射(示例数据,后续从接口获取)
|
||
const dateStatusMap = ref({
|
||
'2025-12-01': '上班未打卡',
|
||
'2025-11-30': '下班未打卡',
|
||
// 可以添加更多日期状态
|
||
})
|
||
|
||
// 当前月份文本
|
||
const currentMonthText = computed(() => {
|
||
return currentMonth.value.format('YYYY年MM月')
|
||
})
|
||
|
||
// 选中的日期文本
|
||
const selectedDateText = computed(() => {
|
||
return selectedDate.value.format('YYYY年MM月DD日')
|
||
})
|
||
|
||
// 选中日期的状态
|
||
const selectedDateStatus = computed(() => {
|
||
const dateKey = selectedDate.value.format('YYYY-MM-DD')
|
||
return dateStatusMap.value[dateKey] || '正常'
|
||
})
|
||
|
||
// 日历天数数组
|
||
const calendarDays = computed(() => {
|
||
const days = []
|
||
const startOfMonth = currentMonth.value.startOf('month')
|
||
const endOfMonth = currentMonth.value.endOf('month')
|
||
const startDay = startOfMonth.day() // 0-6, 0是星期日
|
||
|
||
// 上个月的日期
|
||
for (let i = startDay - 1; i >= 0; i--) {
|
||
const date = dayjs(startOfMonth).subtract(i + 1, 'day')
|
||
const dateKey = date.format('YYYY-MM-DD')
|
||
days.push({
|
||
date: date.toDate(),
|
||
day: date.date(),
|
||
isCurrentMonth: false,
|
||
isSelected: false,
|
||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||
})
|
||
}
|
||
|
||
// 当前月的日期
|
||
for (let i = 0; i < endOfMonth.date(); i++) {
|
||
const date = dayjs(startOfMonth).add(i, 'day')
|
||
const dateKey = date.format('YYYY-MM-DD')
|
||
const isSelected = date.isSame(selectedDate.value, 'day')
|
||
days.push({
|
||
date: date.toDate(),
|
||
day: date.date(),
|
||
isCurrentMonth: true,
|
||
isSelected,
|
||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||
})
|
||
}
|
||
|
||
// 下个月的日期(补齐到42个格子,6行x7列)
|
||
const remainingDays = 42 - days.length
|
||
for (let i = 1; i <= remainingDays; i++) {
|
||
const date = dayjs(endOfMonth).add(i, 'day')
|
||
const dateKey = date.format('YYYY-MM-DD')
|
||
days.push({
|
||
date: date.toDate(),
|
||
day: date.date(),
|
||
isCurrentMonth: false,
|
||
isSelected: false,
|
||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||
})
|
||
}
|
||
|
||
return days
|
||
})
|
||
|
||
/**
|
||
* 处理日期点击
|
||
* @param {Object} dateObj - 日期对象
|
||
*/
|
||
const handleDateClick = (dateObj) => {
|
||
selectedDate.value = dayjs(dateObj.date)
|
||
// TODO: 加载该日期的详细考勤信息
|
||
loadDateDetail(dateObj.date)
|
||
}
|
||
|
||
/**
|
||
* 切换到上一个月
|
||
*/
|
||
const handlePrevMonth = () => {
|
||
currentMonth.value = dayjs(currentMonth.value).subtract(1, 'month')
|
||
// TODO: 重新加载该月的考勤数据
|
||
loadMonthData()
|
||
}
|
||
|
||
/**
|
||
* 切换到下一个月
|
||
*/
|
||
const handleNextMonth = () => {
|
||
currentMonth.value = dayjs(currentMonth.value).add(1, 'month')
|
||
// TODO: 重新加载该月的考勤数据
|
||
loadMonthData()
|
||
}
|
||
|
||
/**
|
||
* 加载月份数据
|
||
*/
|
||
const loadMonthData = () => {
|
||
// TODO: 调用接口获取该月的考勤数据
|
||
// const res = await getAttendanceData(currentMonth.value.format('YYYY-MM'))
|
||
// abnormalDates.value = res.abnormalDates
|
||
// dateStatusMap.value = res.dateStatusMap
|
||
}
|
||
|
||
/**
|
||
* 加载日期详情
|
||
* @param {Date} date - 日期对象
|
||
*/
|
||
const loadDateDetail = (date) => {
|
||
// TODO: 调用接口获取该日期的详细考勤信息
|
||
// const res = await getDateDetail(dayjs(date).format('YYYY-MM-DD'))
|
||
// 更新日期状态
|
||
}
|
||
|
||
/**
|
||
* 返回上一页
|
||
*/
|
||
const handleBack = () => {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 默认选中今天
|
||
selectedDate.value = dayjs()
|
||
// 加载当前月数据
|
||
loadMonthData()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page-container {
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #f5f7fa;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.content-wrapper {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-y: auto;
|
||
background: #fff;
|
||
}
|
||
|
||
.calendar-section {
|
||
padding: 32rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.month-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.month-nav-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 50%;
|
||
background: #f5f5f5;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.month-nav-btn:active {
|
||
background: #e5e5e5;
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.month-text {
|
||
font-size: 36rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.weekdays-header {
|
||
display: flex;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.weekday-cell {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 16rpx 0;
|
||
}
|
||
|
||
.weekday-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.date-cell {
|
||
width: calc(100% / 7);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 16rpx 0;
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.date-number {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.date-cell.other-month .date-number {
|
||
color: #ccc;
|
||
}
|
||
|
||
.date-cell.selected {
|
||
background: #07c160;
|
||
// border-radius: 50%;
|
||
// width: 64rpx;
|
||
// height: 64rpx;
|
||
}
|
||
|
||
.date-cell.selected .date-number {
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.abnormal-label {
|
||
font-size: 20rpx;
|
||
color: #e34d59;
|
||
line-height: 1;
|
||
}
|
||
|
||
.date-cell.selected .abnormal-label {
|
||
color: #fff;
|
||
}
|
||
|
||
.detail-section {
|
||
padding: 32rpx;
|
||
background: #fff;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
.detail-date {
|
||
font-size: 32rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
margin-bottom: 24rpx;
|
||
display: block;
|
||
}
|
||
|
||
.detail-content {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
background: #f5f5f5;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.detail-line {
|
||
width: 4rpx;
|
||
height: 100%;
|
||
background: #e34d59;
|
||
border-radius: 2rpx;
|
||
margin-right: 24rpx;
|
||
position: absolute;
|
||
left: 32rpx;
|
||
top: 0;
|
||
}
|
||
|
||
.detail-status {
|
||
font-size: 28rpx;
|
||
color: #e34d59;
|
||
margin-left: 28rpx;
|
||
flex: 1;
|
||
}
|
||
|
||
.back-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.back-btn:active {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
transform: scale(0.95);
|
||
}
|
||
</style>
|