Files
schedule/src/components/CalendarView.vue
2025-06-11 15:42:48 +08:00

420 lines
13 KiB
Vue

<template>
<div class="bg-gradient-to-br from-white to-gray-50 rounded-2xl shadow-soft p-6">
<!-- 月视图 -->
<div v-if="view === 'month'" class="month-view" data-aos="fade-up" data-aos-duration="500">
<div class="grid grid-cols-7 gap-1">
<div
v-for="day in ['一', '二', '三', '四', '五', '六', '日']"
:key="day"
class="text-center py-3 font-semibold text-gray-700 bg-gray-50 rounded-lg"
>
{{ day }}
</div>
</div>
<div class="grid grid-cols-7 mt-3">
<div
v-for="(day, index) in monthDays"
:key="index"
@click="selectDate(day.date)"
@dragover.prevent="handleDragOver($event, day.date)"
@dragleave="handleDragLeave"
@drop="handleDropEvent($event, day.date)"
:class="{
'bg-primary-50': selectedDate === day.date,
'active-day': selectedDate === day.date,
'drag-over': isDragOverCell === day.date
}"
class="calendar-cell dropzone rounded-xl border border-gray-100 p-3 cursor-pointer transition-all relative min-h-[120px] hover:shadow-md"
>
<!-- 日期和工作状态 -->
<div class="flex justify-between items-start">
<span
class="text-lg font-semibold w-8 h-8 flex items-center justify-center rounded-full relative"
:class="{
'bg-primary-600 text-white': isToday(day.date),
'text-gray-900': day.currentMonth && !isToday(day.date),
'text-gray-400': !day.currentMonth
}"
>
{{ day.date.split('-')[2] }}
</span>
<!-- 农历日期 -->
<span class="lunar-date text-xs text-gray-500 mt-1" style="position: absolute; right: 5px; bottom: 5px">
{{ getLunarDate(day.date) }}
</span>
<span v-if="isHoliday(day.date)" class="work-marker day-off"></span>
<span v-else-if="isWorkDayOnWeekend(day.date)" class="work-marker work-day"></span>
</div>
<!-- 节假日/节气 -->
<div class="mt-2">
<div v-if="getFestival(day.date)" class="flex items-center">
<span class="holiday-tag">
<i class="fas fa-star mr-1"></i> {{ getFestival(day.date) }}
</span>
</div>
<div v-if="getSolarTerm(day.date)" class="solar-term mt-1">
{{ getSolarTerm(day.date) }}
</div>
</div>
<!-- 事件展示 -->
<div v-if="hasEvent(day.date)" class="mt-2 text-xs max-w-full overflow-hidden">
<div
v-for="(event, idx) in getEventsForDate(day.date).slice(0, 2)"
:key="idx"
class="truncate mb-1 flex items-center"
>
<span
class="event-dot mr-1"
:style="{ backgroundColor: event.color }"
></span>
{{ event.title }}
</div>
<div v-if="getEventsForDate(day.date).length > 2" class="text-gray-500">
+{{ getEventsForDate(day.date).length - 2 }}个日程
</div>
</div>
</div>
</div>
</div>
<!-- 周视图 -->
<div v-else-if="view === 'week'" class="week-view" data-aos="fade-up" data-aos-duration="500">
<div class="flex mb-3 bg-gray-50 rounded-lg">
<div class="w-20"></div>
<div
v-for="(day, index) in weekDays"
:key="index"
@click="selectDate(day.format('YYYY-MM-DD'))"
:class="{'bg-primary-100': selectedDate === day.format('YYYY-MM-DD')}"
class="flex-1 text-center py-3 rounded-lg transition-colors cursor-pointer hover:bg-gray-100"
>
<p class="font-medium text-gray-700">{{ day.format('ddd') }}</p>
<p
class="mx-auto w-10 h-10 flex items-center justify-center rounded-full mt-1 text-lg"
:class="{
'bg-primary-600 text-white': isToday(day.format('YYYY-MM-DD')),
'text-gray-900': !isToday(day.format('YYYY-MM-DD'))
}"
>
{{ day.date() }}
</p>
<div class="text-xs text-gray-500 mt-1">
{{ getLunarDate(day.format('YYYY-MM-DD')) }}
</div>
</div>
</div>
<div class="border border-gray-200 rounded-xl overflow-hidden">
<div
v-for="hour in 12"
:key="hour"
class="flex"
>
<div class="w-20 py-3 text-center text-sm text-gray-600 bg-gray-50 flex-shrink-0">
{{ hour + 8 }}:00
</div>
<div
v-for="(day, index) in weekDays"
:key="index"
@dragover.prevent="handleDragOver($event, day.format('YYYY-MM-DD'), (hour + 8) + ':00')"
@dragleave="handleDragLeave"
@drop="handleDropEvent($event, day.format('YYYY-MM-DD'), (hour + 8) + ':00')"
:class="{
'bg-gray-50': index % 2 === 0,
'border-l border-gray-200': index > 0,
'drag-over': isDragOverCell === day.format('YYYY-MM-DD') + '-' + (hour + 8)
}"
class="flex-1 min-h-16 p-2 dropzone"
>
<div
v-for="event in getEventsForDate(day.format('YYYY-MM-DD'), (hour + 8) + ':00')"
:key="event.id"
@click="showEventDetail(event)"
class="text-xs p-2 rounded-lg mb-1 truncate cursor-pointer hover:shadow-sm"
:style="{
backgroundColor: `${event.color}10`,
borderLeft: `3px solid ${event.color}`
}"
>
{{ event.title }}
</div>
</div>
</div>
</div>
</div>
<!-- 日视图 -->
<div v-else class="day-view" data-aos="fade-up" data-aos-duration="500">
<div
class="flex items-center justify-center py-3 mb-4 rounded-xl day-header"
@click="selectDate(currentDate)"
>
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-800">
{{ currentDay.format('YYYY年MM月DD日') }}
</h2>
<p class="text-gray-600 mt-1 flex items-center justify-center space-x-2">
<span>{{ currentDay.format('dddd') }}</span>
<span v-if="isHoliday(currentDay.format('YYYY-MM-DD'))" class="bg-red-100 text-red-600 px-2 py-1 rounded-full text-sm">休假日</span>
<span v-else class="bg-green-100 text-green-600 px-2 py-1 rounded-full text-sm">工作日</span>
</p>
<div class="text-sm text-gray-500 mt-1">
{{ getLunarDate(currentDay.format('YYYY-MM-DD')) }}
</div>
</div>
</div>
<div class="border border-gray-200 rounded-xl overflow-hidden">
<div
v-for="hour in 14"
:key="hour"
class="flex border-b border-gray-200 last:border-0"
>
<div class="w-20 py-4 text-center text-sm text-gray-600 bg-gray-50 flex-shrink-0">
{{ hour + 7 }}:00
</div>
<div
@dragover.prevent="handleDragOver($event, currentDay.format('YYYY-MM-DD'), (hour + 7) + ':00')"
@dragleave="handleDragLeave"
@drop="handleDropEvent($event, currentDay.format('YYYY-MM-DD'), (hour + 7) + ':00')"
:class="{
'drag-over': isDragOverCell === currentDay.format('YYYY-MM-DD') + '-' + (hour + 7)
}"
class="flex-1 min-h-16 p-3 dropzone"
>
<div
v-for="event in getEventsForDate(currentDay.format('YYYY-MM-DD'), (hour + 7) + ':00')"
:key="event.id"
draggable="true"
@dragstart="handleDragStart($event, event, 'event')"
@dragend="handleDragEnd"
class="schedule-item mb-2 p-3 rounded-lg shadow-sm cursor-pointer border-l-4 hover:shadow-md"
:style="{
backgroundColor: `${event.color}10`,
borderLeftColor: event.color
}"
>
<div class="flex justify-between">
<span class="font-medium">{{ event.title }}</span>
<span class="text-xs" :style="{ color: event.color }">{{ event.startTime }}</span>
</div>
<div v-if="event.location" class="text-xs mt-1 text-gray-500 flex items-center">
<i class="fas fa-location-dot mr-1 text-xs"></i> {{ event.location }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, defineProps, defineEmits, ref } from 'vue';
import dayjs from 'dayjs';
import { getLunarDate, getFestival, getSolarTerm, isHoliday } from '@/utils/calendarUtils';
const props = defineProps({
view: String,
currentDate: String,
selectedDate: String,
events: Array,
commonEvents: Array
});
const emit = defineEmits(['select-date', 'show-event-detail', 'drop-event', 'drag-start', 'drag-end']);
// 拖拽状态
const isDragOverCell = ref(null);
// 计算月视图日期
const monthDays = computed(() => {
const startDay = dayjs(props.currentDate).startOf('month').startOf('week');
const endDay = dayjs(props.currentDate).endOf('month').endOf('week');
const days = [];
for (let day = startDay; day.isBefore(endDay); day = day.add(1, 'day')) {
days.push({
date: day.format('YYYY-MM-DD'),
currentMonth: day.month() === dayjs(props.currentDate).month()
});
}
return days;
});
// 计算周视图日期
const weekDays = computed(() => {
const start = dayjs(props.currentDate).startOf('week');
const days = [];
for (let i = 0; i < 7; i++) {
days.push(start.add(i, 'day'));
}
return days;
});
// 计算日视图的当前日期
const currentDay = computed(() => {
return dayjs(props.currentDate);
});
// 检查日期是否有事件
const hasEvent = (date) => {
return props.events.some(e => e.date === date);
};
// 获取当天的所有事件
const getEventsForDate = (date, time = null) => {
let filteredEvents = props.events.filter(e => e.date === date);
if (time) {
filteredEvents = filteredEvents.filter(e => e.startTime === time);
}
return filteredEvents;
};
// 检查是否是今天
const isToday = (date) => {
return dayjs(date).isSame(dayjs(), 'day');
};
// 是否为调休工作日
const isWorkDayOnWeekend = (date) => {
return false;
};
const selectDate = (date) => {
emit('select-date', date);
};
const showEventDetail = (event) => {
emit('show-event-detail', event);
};
// 处理拖拽开始
const handleDragStart = (event, item, type) => {
emit('drag-start', event, item, type);
};
// 处理拖拽结束
const handleDragEnd = () => {
emit('drag-end');
};
// 处理拖拽悬停
const handleDragOver = (event, date, time = null) => {
// 生成唯一标识,用于标记当前拖拽悬停的单元格
if (time) {
isDragOverCell.value = `${date}-${time}`;
} else {
isDragOverCell.value = date;
}
event.preventDefault();
};
// 处理拖拽离开
const handleDragLeave = (event) => {
isDragOverCell.value = null;
};
// 处理放置事件
const handleDropEvent = (event, date, time = null) => {
isDragOverCell.value = null;
emit('drop-event', event, date, time);
};
</script>
<style scoped>
.calendar-cell {
transition: all 0.3s ease;
}
.calendar-cell:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.dropzone.drag-over {
background-color: rgba(100, 94, 255, 0.3);
box-shadow: inset 0 0 0 2px #645eff;
}
.dropzone.active-day {
background-color: rgba(100, 94, 255, 0.3);
box-shadow: inset 0 0 0 2px #645eff;
/* 用虚线美化边框 */
border-bottom: 1px dashed rgba(100, 94, 255, 0.5);
border-radius: 10px;
}
.work-marker {
position: absolute;
top: 6px;
right: 8px;
font-size: 0.75rem;
padding: 1px 5px;
border-radius: 4px;
font-weight: 600;
}
.day-off {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.work-day {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.solar-term {
font-size: 0.75rem;
color: #059669;
background: rgba(5, 150, 105, 0.1);
padding: 2px 8px;
border-radius: 9999px;
margin-top: 4px;
display: inline-block;
}
.event-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
}
.holiday-tag {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
padding: 0.15rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
line-height: 1rem;
}
.schedule-item {
transition: all 0.3s ease;
cursor: grab;
}
.schedule-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
}
.day-header {
background: linear-gradient(to right, #f0f4ff, #e0e7ff);
}
</style>