420 lines
13 KiB
Vue
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>
|