|
|
@@ -0,0 +1,270 @@
|
|
|
+<template>
|
|
|
+ <div class="holiday-calendar">
|
|
|
+ <ContentWrap>
|
|
|
+ <div class="controls">
|
|
|
+ <el-select v-model="selectedYear" placeholder="选择年份" @change="handleYearChange">
|
|
|
+ <el-option
|
|
|
+ v-for="year in yearOptions"
|
|
|
+ :key="year"
|
|
|
+ :label="year"
|
|
|
+ :value="year"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ <el-checkbox-group v-model="showOptions" class="display-options">
|
|
|
+ <el-checkbox label="weekend">显示周末</el-checkbox>
|
|
|
+ <el-checkbox label="legal">显示法定假日</el-checkbox>
|
|
|
+ <el-checkbox label="holiday">显示节假日</el-checkbox>
|
|
|
+ <el-checkbox label="workday">显示调休上班</el-checkbox>
|
|
|
+ </el-checkbox-group>
|
|
|
+ </div>
|
|
|
+ </ContentWrap>
|
|
|
+
|
|
|
+ <div class="calendar-wrapper">
|
|
|
+ <el-calendar v-model="selectedDate">
|
|
|
+ <template #date-cell="{ data }">
|
|
|
+ <div class="calendar-day" :class="getDayClasses(data.day)">
|
|
|
+ <div class="day-number">
|
|
|
+ {{ getDayNumber(data.day) }}
|
|
|
+ <!-- 如果是正常工作日显示圆圈“班” -->
|
|
|
+ <span v-if="isNormalWorkday(data.day)" class="work-circle">班</span>
|
|
|
+ </div>
|
|
|
+ <div class="day-info">
|
|
|
+ <div class="holiday-name" v-if="getHolidayName(data.day)">
|
|
|
+ {{ getHolidayName(data.day) }}
|
|
|
+ </div>
|
|
|
+ <template v-if="showOptions.includes('weekend') && isWeekend(data.day)">
|
|
|
+ <el-tag size="small" effect="plain" type="info">周末</el-tag>
|
|
|
+ </template>
|
|
|
+ <template v-if="showOptions.includes('legal') && isLegalHoliday(data.day)">
|
|
|
+ <el-tag size="small" effect="plain" type="danger">法定假日</el-tag>
|
|
|
+ </template>
|
|
|
+ <template v-if="showOptions.includes('holiday') && isHoliday(data.day)">
|
|
|
+ <el-tag size="small" effect="plain" type="success">节假日</el-tag>
|
|
|
+ </template>
|
|
|
+ <template v-if="showOptions.includes('workday') && isWorkday(data.day)">
|
|
|
+ <el-tag size="small" effect="plain" type="warning">调休上班</el-tag>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-calendar>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted, watch } from 'vue';
|
|
|
+import axios from 'axios';
|
|
|
+
|
|
|
+// 当前年
|
|
|
+const currentYear = new Date().getFullYear();
|
|
|
+const selectedYear = ref(currentYear);
|
|
|
+const selectedDate = ref(new Date());
|
|
|
+const holidayData = ref({});
|
|
|
+const showOptions = ref(['weekend', 'legal', 'holiday', 'workday']);
|
|
|
+
|
|
|
+// 年份选项
|
|
|
+const yearOptions = computed(() => {
|
|
|
+ const years = [];
|
|
|
+ for (let year = currentYear; year <= 2050; year++) {
|
|
|
+ years.push(year);
|
|
|
+ }
|
|
|
+ return years;
|
|
|
+});
|
|
|
+
|
|
|
+// 拉节假日接口,失败则用默认
|
|
|
+const fetchHolidayData = async (year) => {
|
|
|
+ try {
|
|
|
+ const res = await axios.get(`https://fastly.jsdelivr.net/gh/NateScarlet/holiday-cn@master/${year}.json`);
|
|
|
+ const days = res.data?.days || [];
|
|
|
+ if (Array.isArray(days) && days.length > 0) {
|
|
|
+ const dayMap = {};
|
|
|
+ days.forEach(item => {
|
|
|
+ dayMap[item.date] = {
|
|
|
+ name: item.name,
|
|
|
+ isOffDay: item.isOffDay,
|
|
|
+ isLegal: item.isOffDay // 简单视为法定
|
|
|
+ };
|
|
|
+ });
|
|
|
+ holidayData.value[year] = dayMap;
|
|
|
+ } else {
|
|
|
+ console.warn(`无 ${year} 节假日数据,用默认`);
|
|
|
+ holidayData.value[year] = generateDefaultHolidays(year);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error(`获取 ${year} 节假日失败,使用默认`, err);
|
|
|
+ holidayData.value[year] = generateDefaultHolidays(year);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 默认假期
|
|
|
+const generateDefaultHolidays = (year) => {
|
|
|
+ const holidays = {};
|
|
|
+ holidays[`${year}-01-01`] = { name: '元旦', isOffDay: true, isLegal: true };
|
|
|
+ for (let i = 0; i < 7; i++) {
|
|
|
+ const date = new Date(year, 0, 22 + i);
|
|
|
+ holidays[formatDateKey(date)] = { name: '春节', isOffDay: true, isLegal: i < 3 };
|
|
|
+ }
|
|
|
+ holidays[`${year}-04-04`] = { name: '清明节', isOffDay: true, isLegal: true };
|
|
|
+ for (let i = 0; i < 3; i++) {
|
|
|
+ holidays[`${year}-05-0${1 + i}`] = { name: '劳动节', isOffDay: true, isLegal: i === 0 };
|
|
|
+ }
|
|
|
+ holidays[`${year}-09-29`] = { name: '中秋节', isOffDay: true, isLegal: true };
|
|
|
+ for (let i = 1; i <= 7; i++) {
|
|
|
+ holidays[`${year}-10-${i < 10 ? '0' + i : i}`] = {
|
|
|
+ name: '国庆节',
|
|
|
+ isOffDay: true,
|
|
|
+ isLegal: i <= 3
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return holidays;
|
|
|
+};
|
|
|
+
|
|
|
+// 工具
|
|
|
+const formatDateKey = (date) => {
|
|
|
+ const y = date.getFullYear();
|
|
|
+ const m = date.getMonth() + 1;
|
|
|
+ const d = date.getDate();
|
|
|
+ return `${y}-${m < 10 ? '0' + m : m}-${d < 10 ? '0' + d : d}`;
|
|
|
+};
|
|
|
+
|
|
|
+const isWeekend = (dateStr) => {
|
|
|
+ const d = new Date(dateStr).getDay();
|
|
|
+ return d === 0 || d === 6;
|
|
|
+};
|
|
|
+const isLegalHoliday = (dateStr) => {
|
|
|
+ const k = formatDateKey(new Date(dateStr));
|
|
|
+ return holidayData.value[selectedYear.value]?.[k]?.isLegal || false;
|
|
|
+};
|
|
|
+const isHoliday = (dateStr) => {
|
|
|
+ const k = formatDateKey(new Date(dateStr));
|
|
|
+ return holidayData.value[selectedYear.value]?.[k]?.isOffDay || false;
|
|
|
+};
|
|
|
+const isWorkday = (dateStr) => {
|
|
|
+ const k = formatDateKey(new Date(dateStr));
|
|
|
+ const isWeekendDay = isWeekend(dateStr);
|
|
|
+ const isHolidayDay = holidayData.value[selectedYear.value]?.[k]?.isOffDay || false;
|
|
|
+ return isWeekendDay && !isHolidayDay;
|
|
|
+};
|
|
|
+// ✅ 新增:正常工作日
|
|
|
+const isNormalWorkday = (dateStr) => {
|
|
|
+ return (
|
|
|
+ !isWeekend(dateStr) &&
|
|
|
+ !isLegalHoliday(dateStr) &&
|
|
|
+ !isHoliday(dateStr)
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const getDayClasses = (dateStr) => {
|
|
|
+ const c = [];
|
|
|
+ if (isWeekend(dateStr)) c.push('weekend');
|
|
|
+ if (isLegalHoliday(dateStr)) c.push('legal-holiday');
|
|
|
+ if (isHoliday(dateStr)) c.push('holiday');
|
|
|
+ if (isWorkday(dateStr)) c.push('workday');
|
|
|
+ return c;
|
|
|
+};
|
|
|
+const getDayNumber = (dateStr) => new Date(dateStr).getDate();
|
|
|
+const getHolidayName = (dateStr) => {
|
|
|
+ const k = formatDateKey(new Date(dateStr));
|
|
|
+ return holidayData.value[selectedYear.value]?.[k]?.name || '';
|
|
|
+};
|
|
|
+
|
|
|
+const handleYearChange = (year) => {
|
|
|
+ selectedDate.value = new Date(year, 0, 1);
|
|
|
+ if (!holidayData.value[year]) {
|
|
|
+ fetchHolidayData(year);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 监听跨年
|
|
|
+watch(selectedDate, (newDate) => {
|
|
|
+ const newYear = newDate.getFullYear();
|
|
|
+ if (newYear !== selectedYear.value) {
|
|
|
+ selectedYear.value = newYear;
|
|
|
+ if (!holidayData.value[newYear]) {
|
|
|
+ fetchHolidayData(newYear);
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchHolidayData(currentYear);
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.holiday-calendar {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.controls {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-wrapper {
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-calendar {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-day {
|
|
|
+ height: 100%;
|
|
|
+ padding: 5px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.day-number {
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.work-circle {
|
|
|
+ display: inline-block;
|
|
|
+ background: #409EFF;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 10px;
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ line-height: 18px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.day-info {
|
|
|
+ min-height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.holiday-name {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #f56c6c;
|
|
|
+ margin-bottom: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-tag {
|
|
|
+ margin: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.weekend {
|
|
|
+ background-color: #f0f9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.legal-holiday {
|
|
|
+ background-color: #fff0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.holiday {
|
|
|
+ background-color: #f0fff0;
|
|
|
+}
|
|
|
+
|
|
|
+.workday {
|
|
|
+ background-color: #fffaf0;
|
|
|
+}
|
|
|
+</style>
|