429 lines
11 KiB
Vue
429 lines
11 KiB
Vue
<template>
|
|
<div class="attendance-calendar">
|
|
<div class="legend">
|
|
<div class="legend-item">
|
|
<span class="legend-mark active"></span>
|
|
<span>已打卡</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-mark inactive"></span>
|
|
<span>未打卡</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-mark not-in-range"></span>
|
|
<span>补卡</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<span class="legend-mark inactive-no-att"></span>
|
|
<span>不在场</span>
|
|
</div>
|
|
</div>
|
|
<!-- 循环展示每个月份的日历 -->
|
|
<div
|
|
:key="index"
|
|
class="month-calendar"
|
|
v-for="(monthData, index) in monthDataListNew"
|
|
>
|
|
<!-- 月份标题 -->
|
|
<div class="calendar-header">
|
|
<h3>{{ monthData.year }}年 {{ monthData.month }}月</h3>
|
|
</div>
|
|
|
|
<!-- 星期标题 -->
|
|
<div class="week-days">
|
|
<div class="week-day" v-for="day in weekDays" :key="day">
|
|
{{ day }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 日历主体 -->
|
|
<div class="calendar-grid">
|
|
<div
|
|
class="calendar-day empty"
|
|
:key="'empty-start-' + i"
|
|
v-for="(empty, i) in monthData.emptyStartDays"
|
|
>
|
|
</div>
|
|
|
|
<div
|
|
:key="day.dayIndex"
|
|
:class="getDayClass(day)"
|
|
class="calendar-day current-month"
|
|
v-for="day in monthData.currentMonthDays"
|
|
>
|
|
<span class="day-number">{{ day.dayIndex }}</span>
|
|
</div>
|
|
|
|
<div
|
|
:key="'empty-end-' + i"
|
|
class="calendar-day empty"
|
|
v-for="(empty, i) in monthData.emptyEndDays"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: 'AttendanceCalendar',
|
|
props: {
|
|
// 时间范围
|
|
timeRange: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
|
|
// 考勤列表
|
|
attendanceList: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
weekDays: ['日', '一', '二', '三', '四', '五', '六'],
|
|
checkedDays: {}, // 存储复选框状态
|
|
checked: false,
|
|
monthDataListNew: [], // 日历的月份数据
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
// 初始化复选框状态
|
|
initCheckedDays() {
|
|
this.monthDataList.forEach((monthData) => {
|
|
monthData.day.forEach((day) => {
|
|
if (day.isActive === 1) {
|
|
const key = `${monthData.year}-${monthData.month}-${day.dayIndex}`
|
|
this.$set(this.checkedDays, key, false)
|
|
}
|
|
})
|
|
})
|
|
},
|
|
|
|
// 初始化日历
|
|
initCalendar(timeRange) {
|
|
const startMonth = timeRange[0].split('-')[1]
|
|
const endMonth = timeRange[1].split('-')[1]
|
|
const year = timeRange[0].split('-')[0]
|
|
|
|
this.monthDataListNew = []
|
|
|
|
if (startMonth !== endMonth) {
|
|
this.monthDataListNew.push({
|
|
month: startMonth,
|
|
year,
|
|
emptyStartDays: this.getFirstMonthEmptyDays({
|
|
month: startMonth,
|
|
year,
|
|
}),
|
|
emptyEndDays: this.getLastMonthEmptyDays({
|
|
month: startMonth,
|
|
year,
|
|
}),
|
|
currentMonthDays: this.getDaysInMonthNew({
|
|
month: startMonth,
|
|
year,
|
|
}),
|
|
})
|
|
this.monthDataListNew.push({
|
|
month: endMonth,
|
|
year,
|
|
emptyStartDays: this.getFirstMonthEmptyDays({
|
|
month: endMonth,
|
|
year,
|
|
}),
|
|
emptyEndDays: this.getLastMonthEmptyDays({
|
|
month: endMonth,
|
|
year,
|
|
}),
|
|
currentMonthDays: this.getDaysInMonthNew({
|
|
month: endMonth,
|
|
year,
|
|
}),
|
|
})
|
|
} else {
|
|
this.monthDataListNew.push({
|
|
month: startMonth,
|
|
year,
|
|
emptyStartDays: this.getFirstMonthEmptyDays({
|
|
month: startMonth,
|
|
year,
|
|
}),
|
|
emptyEndDays: this.getLastMonthEmptyDays({
|
|
month: startMonth,
|
|
year,
|
|
}),
|
|
currentMonthDays: this.getDaysInMonthNew({
|
|
month: startMonth,
|
|
year,
|
|
}),
|
|
})
|
|
}
|
|
},
|
|
|
|
// 设置考勤列表信息
|
|
initAttendanceList(attendanceList) {
|
|
if (attendanceList.length > 0) {
|
|
attendanceList.forEach((item) => {
|
|
const year = item.einDay.split('-')[0]
|
|
const month = item.einDay.split('-')[1]
|
|
|
|
this.monthDataListNew.forEach((j) => {
|
|
if (j.year == year && j.month == month) {
|
|
j.currentMonthDays.forEach((k) => {
|
|
if (k.zeroIndex == item.einDay.split('-')[2]) {
|
|
k.isAtt = item.isAtt
|
|
k.isRepair = item.isRepair
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
}
|
|
},
|
|
|
|
// 获取指定月份的天数
|
|
getDaysInMonth(monthData) {
|
|
const year = parseInt(monthData.year)
|
|
const month = parseInt(monthData.month)
|
|
const days = new Date(year, month, 0).getDate()
|
|
return days
|
|
},
|
|
|
|
getDaysInMonthNew(monthData) {
|
|
const year = parseInt(monthData.year)
|
|
const month = parseInt(monthData.month)
|
|
const days = new Date(year, month, 0).getDate()
|
|
const dayList = []
|
|
for (let i = 1; i <= days * 1; i++) {
|
|
dayList.push({
|
|
dayIndex: i,
|
|
isAtt: null,
|
|
isRepair: false,
|
|
zeroIndex: i > 10 ? i : `0${i}`,
|
|
})
|
|
}
|
|
return dayList
|
|
},
|
|
|
|
// 获取月份第一天是星期几
|
|
getFirstDayOfMonth(monthData) {
|
|
const year = parseInt(monthData.year)
|
|
const month = parseInt(monthData.month) - 1 // 月份是0-based
|
|
return new Date(year, month, 1).getDay()
|
|
},
|
|
|
|
// 计算月初需要的空白格子数量
|
|
getFirstMonthEmptyDays(monthData) {
|
|
return this.getFirstDayOfMonth(monthData)
|
|
},
|
|
// 计算月末需要的空白格子数量
|
|
getLastMonthEmptyDays(monthData) {
|
|
console.log(monthData, 'monthData----')
|
|
const totalDays = this.getDaysInMonth(monthData)
|
|
|
|
const lastDay = new Date(
|
|
parseInt(monthData.year),
|
|
parseInt(monthData.month) - 1,
|
|
totalDays,
|
|
).getDay()
|
|
return 6 - lastDay
|
|
},
|
|
|
|
// 获取某天的样式类
|
|
getDayClass(day) {
|
|
switch (day.isAtt) {
|
|
case 0:
|
|
return 'inactive'
|
|
case 1:
|
|
return 'active'
|
|
case 2:
|
|
return 'active-repair'
|
|
}
|
|
return 'is-no-att'
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
timeRange: {
|
|
handler(newVal) {
|
|
console.log(newVal, 'newVal')
|
|
this.initCalendar(newVal)
|
|
},
|
|
deep: true,
|
|
immediate: true,
|
|
},
|
|
|
|
attendanceList: {
|
|
handler(newVal) {
|
|
this.initAttendanceList(newVal)
|
|
},
|
|
deep: true,
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.attendance-calendar {
|
|
font-family: 'Arial', sans-serif;
|
|
max-width: 90%;
|
|
min-width: 460px;
|
|
|
|
padding: 20px;
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.month-calendar {
|
|
margin-bottom: 30px;
|
|
padding-bottom: 20px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.month-calendar:last-child {
|
|
margin-bottom: 0;
|
|
padding-bottom: 0;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.calendar-header {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding: 0 10px;
|
|
}
|
|
|
|
.calendar-header h3 {
|
|
margin: 0;
|
|
color: #333;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.week-days {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.week-day {
|
|
text-align: center;
|
|
font-weight: bold;
|
|
color: #666;
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.calendar-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
gap: 5px;
|
|
}
|
|
|
|
.calendar-day {
|
|
aspect-ratio: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 6px;
|
|
position: relative;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.empty {
|
|
background-color: #f9f9f9;
|
|
border: 1px solid transparent;
|
|
}
|
|
|
|
.current-month {
|
|
border: 1px solid #eee;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* 非考勤范围样式 */
|
|
.not-in-range {
|
|
background-color: #f0f0f0;
|
|
color: #999;
|
|
}
|
|
|
|
/* 已打卡样式 */
|
|
.active {
|
|
background-color: #19be6b;
|
|
color: #000;
|
|
}
|
|
|
|
/* 未打卡样式 */
|
|
.inactive {
|
|
background-color: #f56c6c;
|
|
color: #000;
|
|
}
|
|
|
|
/* 已补卡样式 */
|
|
.active-repair {
|
|
background-color: #ff9900;
|
|
color: #000;
|
|
}
|
|
|
|
/* 不在场样式 */
|
|
.inactive-no-att {
|
|
background-color: #999;
|
|
color: #000;
|
|
}
|
|
.day-number {
|
|
z-index: 1;
|
|
}
|
|
|
|
/* 复选框样式 */
|
|
.day-checkbox {
|
|
position: absolute;
|
|
top: 2px;
|
|
right: 2px;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* 图例样式 */
|
|
.legend {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.legend-mark {
|
|
display: inline-block;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.legend-mark.active {
|
|
background-color: #19be6b;
|
|
}
|
|
|
|
.legend-mark.inactive {
|
|
background-color: #f56c6c;
|
|
}
|
|
|
|
.legend-mark.not-in-range {
|
|
background-color: #ff9900;
|
|
}
|
|
|
|
.is-no-att {
|
|
background-color: #999 !important;
|
|
}
|
|
</style>
|