2025-06-10 17:03:02 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="min-h-screen w-full bg-gradient-to-br from-gray-50 to-gray-100">
|
|
|
|
|
<!-- 登录模态框 -->
|
|
|
|
|
<LoginModal v-if="showLoginModal" @login="handleLogin" @close="showLoginModal = false" />
|
|
|
|
|
|
|
|
|
|
<!-- 顶部导航 - 粘性布局 -->
|
|
|
|
|
<header class="sticky top-0 z-50 bg-gradient-to-r from-primary-600 to-primary-800 py-5 px-6 rounded-b-2xl shadow-lg">
|
|
|
|
|
<div class="max-w-7xl mx-auto">
|
|
|
|
|
<div class="flex flex-col sm:flex-row items-center justify-between">
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
<div class="w-12 h-12 rounded-xl bg-white bg-opacity-20 flex items-center justify-center shadow-md">
|
|
|
|
|
<i class="fas fa-calendar-alt text-b text-2xl"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h1 class="ml-4 text-2xl font-bold text-b drop-shadow">智能日程管家</h1>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mt-4 sm:mt-0 flex items-center">
|
|
|
|
|
<div class="flex rounded-xl overflow-hidden bg-white bg-opacity-20">
|
|
|
|
|
<button
|
|
|
|
|
@click="changeView('day')"
|
|
|
|
|
:class="{'bg-white text-primary-600': view === 'day'}"
|
|
|
|
|
class="px-4 py-2 font-medium transition-all hover:transform hover:-translate-y-0.5"
|
|
|
|
|
>
|
|
|
|
|
<i class="fas fa-sun mr-2"></i> 日视图
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="changeView('week')"
|
|
|
|
|
:class="{'bg-white text-primary-600': view === 'week'}"
|
|
|
|
|
class="px-4 py-2 font-medium transition-all hover:transform hover:-translate-y-0.5"
|
|
|
|
|
>
|
|
|
|
|
<i class="fas fa-calendar-week mr-2"></i> 周视图
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="changeView('month')"
|
|
|
|
|
:class="{'bg-white text-primary-600': view === 'month'}"
|
|
|
|
|
class="px-4 py-2 font-medium transition-all hover:transform hover:-translate-y-0.5"
|
|
|
|
|
>
|
|
|
|
|
<i class="fas fa-calendar-days mr-2"></i> 月视图
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="ml-4 flex items-center">
|
|
|
|
|
<div class="w-10 h-10 rounded-full overflow-hidden border-2 border-white">
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
<span class="ml-2 text-white text-sm hidden md:block">{{ user.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="max-w-7xl mx-auto px-4 py-8 sm:px-6">
|
|
|
|
|
<!-- 日历导航 -->
|
|
|
|
|
<div data-aos="fade-up" class="flex items-center justify-between mb-8 bg-white rounded-xl p-4 shadow">
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
<button
|
|
|
|
|
@click="changeDate(-1)"
|
|
|
|
|
class="w-10 h-10 flex items-center justify-center rounded-full bg-white hover:bg-gray-100 transition-all shadow hover:scale-105"
|
|
|
|
|
>
|
|
|
|
|
<i class="fas fa-chevron-left text-primary-600"></i>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div class="mx-4">
|
|
|
|
|
<div class="text-xl font-bold text-gray-800 flex items-center">
|
|
|
|
|
{{ displayDate }}
|
|
|
|
|
<span class="ml-3 text-sm px-3 py-1 bg-primary-600 text-white rounded-full">当前视图</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-gray-600 mt-1">{{ dayjs().format('YYYY年MM月') }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
@click="changeDate(1)"
|
|
|
|
|
class="w-10 h-10 flex items-center justify-center rounded-full bg-white hover:bg-gray-100 transition-all shadow hover:scale-105"
|
|
|
|
|
>
|
|
|
|
|
<i class="fas fa-chevron-right text-primary-600"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
@click="currentDate = dayjs().format('YYYY-MM-DD')"
|
|
|
|
|
class="flex items-center px-4 py-2 bg-gradient-to-r from-primary-500 to-primary-700 hover:from-primary-600 hover:to-primary-800 text-white rounded-xl transition-all shadow-lg hover:shadow-xl hover:transform hover:-translate-y-0.5"
|
|
|
|
|
>
|
|
|
|
|
<i class="fas fa-calendar-check mr-2"></i> 返回今天
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 主要内容 -->
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
|
|
|
<!-- 左侧面板 -->
|
|
|
|
|
<div class="lg:col-span-1" data-aos="fade-right" data-aos-delay="100">
|
|
|
|
|
<CommonEvents
|
|
|
|
|
:commonEvents="commonEvents"
|
|
|
|
|
@add-common-event="showCommonEventModal = true"
|
|
|
|
|
@drag-start="handleDragStart"
|
|
|
|
|
@drag-end="handleDragEnd"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 中间面板 - 日历 -->
|
|
|
|
|
<div class="lg:col-span-2" data-aos="fade-up" data-aos-delay="200">
|
|
|
|
|
<CalendarView
|
|
|
|
|
:view="view"
|
|
|
|
|
:currentDate="currentDate"
|
|
|
|
|
:selectedDate="selectedDate"
|
|
|
|
|
:events="events"
|
|
|
|
|
:commonEvents="commonEvents"
|
|
|
|
|
@select-date="selectDate"
|
|
|
|
|
@show-event-detail="showEventDetail"
|
|
|
|
|
@drop-event="handleDropEvent"
|
|
|
|
|
@drag-start="handleDragStart"
|
|
|
|
|
@drag-end="handleDragEnd"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧面板 - 详情 -->
|
|
|
|
|
<div class="lg:col-span-1" data-aos="fade-left" data-aos-delay="300">
|
|
|
|
|
<EventDetail
|
|
|
|
|
:selectedDate="selectedDate"
|
|
|
|
|
:events="events"
|
|
|
|
|
:commonEvents="commonEvents"
|
|
|
|
|
@add-event="showEventModal = true"
|
|
|
|
|
@edit-event="showEventDetail"
|
|
|
|
|
@drop-event="handleDropEvent"
|
|
|
|
|
@drag-start="handleDragStart"
|
|
|
|
|
@drag-end="handleDragEnd"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 模态框 -->
|
|
|
|
|
<EventModal
|
|
|
|
|
v-if="showEventModal"
|
|
|
|
|
:event="editingEvent || newEvent"
|
|
|
|
|
:isEditing="!!editingEvent"
|
|
|
|
|
@close="closeModal"
|
|
|
|
|
@save="saveEvent"
|
|
|
|
|
@delete="deleteEvent"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<CommonEventModal
|
|
|
|
|
v-if="showCommonEventModal"
|
|
|
|
|
@close="closeCommonModal"
|
|
|
|
|
@save="saveCommonEvent"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
2025-04-24 13:39:24 +08:00
|
|
|
<script setup>
|
2025-06-10 17:03:02 +08:00
|
|
|
import { ref, computed, onMounted } from 'vue';
|
|
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import 'dayjs/locale/zh-cn';
|
|
|
|
|
import CalendarView from './components/CalendarView.vue';
|
|
|
|
|
import EventDetail from './components/EventDetail.vue';
|
|
|
|
|
import CommonEvents from './components/CommonEvents.vue';
|
|
|
|
|
import EventModal from './components/EventModal.vue';
|
|
|
|
|
import CommonEventModal from './components/CommonEventModal.vue';
|
|
|
|
|
import LoginModal from './components/LoginModal.vue';
|
|
|
|
|
|
|
|
|
|
dayjs.locale('zh-cn');
|
|
|
|
|
|
|
|
|
|
// 状态管理
|
|
|
|
|
const view = ref('month');
|
|
|
|
|
const currentDate = ref(dayjs().format('YYYY-MM-DD'));
|
|
|
|
|
const selectedDate = ref(dayjs().format('YYYY-MM-DD'));
|
|
|
|
|
const showEventModal = ref(false);
|
|
|
|
|
const showCommonEventModal = ref(false);
|
|
|
|
|
const showLoginModal = ref(true); // 默认显示登录模态框
|
|
|
|
|
const editingEvent = ref(null);
|
|
|
|
|
|
|
|
|
|
// 用户信息
|
|
|
|
|
const user = ref({
|
|
|
|
|
id: '',
|
|
|
|
|
name: '',
|
|
|
|
|
avatar: '',
|
|
|
|
|
phone: '',
|
|
|
|
|
email: '',
|
|
|
|
|
team: '萧康云医技术开发团队-医生组'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 数据
|
|
|
|
|
const events = ref([
|
|
|
|
|
{
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
title: '团队周会',
|
|
|
|
|
date: dayjs().format('YYYY-MM-DD'),
|
|
|
|
|
startTime: '10:00',
|
|
|
|
|
duration: 60,
|
|
|
|
|
location: '会议室A',
|
|
|
|
|
color: '#645eff',
|
|
|
|
|
creator: { id: 'user-1', name: '李齐' },
|
|
|
|
|
participants: [
|
|
|
|
|
{ id: 'user-1', name: '李齐' },
|
|
|
|
|
{ id: 'user-2', name: '张明' },
|
|
|
|
|
{ id: 'user-3', name: '王芳' }
|
|
|
|
|
],
|
|
|
|
|
completed: false
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'event-2',
|
|
|
|
|
title: '午餐会议',
|
|
|
|
|
date: dayjs().format('YYYY-MM-DD'),
|
|
|
|
|
startTime: '12:30',
|
|
|
|
|
duration: 90,
|
|
|
|
|
location: '公司餐厅',
|
|
|
|
|
color: '#0fb8a9',
|
|
|
|
|
creator: { id: 'user-2', name: '张明' },
|
|
|
|
|
participants: [
|
|
|
|
|
{ id: 'user-2', name: '张明' },
|
|
|
|
|
{ id: 'user-4', name: '赵琳' }
|
|
|
|
|
],
|
|
|
|
|
completed: true
|
|
|
|
|
}
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const commonEvents = ref([
|
|
|
|
|
{
|
|
|
|
|
id: 'common-1',
|
|
|
|
|
title: '健身时间',
|
|
|
|
|
duration: 60,
|
|
|
|
|
color: '#e85d75'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'common-2',
|
|
|
|
|
title: '学习时间',
|
|
|
|
|
duration: 90,
|
|
|
|
|
color: '#8c6bed'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'common-3',
|
|
|
|
|
title: '家庭晚餐',
|
|
|
|
|
duration: 120,
|
|
|
|
|
color: '#f0af56'
|
|
|
|
|
}
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 新事件模板
|
|
|
|
|
const newEvent = ref({
|
|
|
|
|
id: null,
|
|
|
|
|
title: '',
|
|
|
|
|
date: selectedDate.value,
|
|
|
|
|
startTime: '10:00',
|
|
|
|
|
duration: 60,
|
|
|
|
|
location: '',
|
|
|
|
|
color: '#645eff',
|
|
|
|
|
creator: null,
|
|
|
|
|
participants: [],
|
|
|
|
|
completed: false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 计算属性
|
|
|
|
|
const displayDate = computed(() => {
|
|
|
|
|
if (view.value === 'day') {
|
|
|
|
|
return dayjs(currentDate.value).format('YYYY年MM月DD日');
|
|
|
|
|
} else if (view.value === 'week') {
|
|
|
|
|
const start = dayjs(currentDate.value).startOf('week');
|
|
|
|
|
const end = start.add(6, 'day');
|
|
|
|
|
return `${start.format('MM月DD日')} - ${end.format('MM月DD日')}`;
|
|
|
|
|
} else {
|
|
|
|
|
return dayjs(currentDate.value).format('YYYY年MM月');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 方法
|
|
|
|
|
const changeView = (v) => {
|
|
|
|
|
view.value = v;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const changeDate = (offset) => {
|
|
|
|
|
if (view.value === 'day') {
|
|
|
|
|
currentDate.value = dayjs(currentDate.value).add(offset, 'day').format('YYYY-MM-DD');
|
|
|
|
|
} else if (view.value === 'week') {
|
|
|
|
|
currentDate.value = dayjs(currentDate.value).add(offset, 'week').format('YYYY-MM-DD');
|
|
|
|
|
} else {
|
|
|
|
|
currentDate.value = dayjs(currentDate.value).add(offset, 'month').format('YYYY-MM-DD');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectDate = (date) => {
|
|
|
|
|
selectedDate.value = date;
|
|
|
|
|
newEvent.value.date = date;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeModal = () => {
|
|
|
|
|
showEventModal.value = false;
|
|
|
|
|
editingEvent.value = null;
|
|
|
|
|
newEvent.value = {
|
|
|
|
|
id: null,
|
|
|
|
|
title: '',
|
|
|
|
|
date: selectedDate.value,
|
|
|
|
|
startTime: '10:00',
|
|
|
|
|
duration: 60,
|
|
|
|
|
location: '',
|
|
|
|
|
color: '#645eff',
|
|
|
|
|
creator: null,
|
|
|
|
|
participants: [],
|
|
|
|
|
completed: false
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeCommonModal = () => {
|
|
|
|
|
showCommonEventModal.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveEvent = (eventData) => {
|
|
|
|
|
if (!eventData.title.trim()) return;
|
|
|
|
|
|
|
|
|
|
if (!editingEvent.value) {
|
|
|
|
|
// 为新事件设置创建者
|
|
|
|
|
eventData.creator = {
|
|
|
|
|
id: user.value.id,
|
|
|
|
|
name: user.value.name
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 自动添加创建者为参与者
|
|
|
|
|
if (!eventData.participants.some(p => p.id === user.value.id)) {
|
|
|
|
|
eventData.participants.push({
|
|
|
|
|
id: user.value.id,
|
|
|
|
|
name: user.value.name
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
eventData.id = 'event-' + Date.now();
|
|
|
|
|
events.value.push({ ...eventData });
|
|
|
|
|
} else {
|
|
|
|
|
const index = events.value.findIndex(e => e.id === editingEvent.value.id);
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
events.value[index] = { ...eventData };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localStorage.setItem('events', JSON.stringify(events.value));
|
|
|
|
|
closeModal();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deleteEvent = () => {
|
|
|
|
|
if (editingEvent.value) {
|
|
|
|
|
events.value = events.value.filter(e => e.id !== editingEvent.value.id);
|
|
|
|
|
localStorage.setItem('events', JSON.stringify(events.value));
|
|
|
|
|
}
|
|
|
|
|
closeModal();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveCommonEvent = (eventData) => {
|
|
|
|
|
if (!eventData.title.trim()) return;
|
|
|
|
|
|
|
|
|
|
commonEvents.value.push({
|
|
|
|
|
...eventData,
|
|
|
|
|
id: 'common-' + Date.now()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
localStorage.setItem('commonEvents', JSON.stringify(commonEvents.value));
|
|
|
|
|
closeCommonModal();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const showEventDetail = (event) => {
|
|
|
|
|
editingEvent.value = event;
|
|
|
|
|
showEventModal.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理拖拽开始
|
|
|
|
|
const handleDragStart = (event, item, type) => {
|
|
|
|
|
event.dataTransfer.setData('type', type);
|
|
|
|
|
event.dataTransfer.setData('data', JSON.stringify(item));
|
|
|
|
|
event.target.classList.add('dragging');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理拖拽结束
|
|
|
|
|
const handleDragEnd = (event) => {
|
|
|
|
|
document.querySelectorAll('.dragging').forEach(el => {
|
|
|
|
|
el.classList.remove('dragging');
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理放置事件
|
|
|
|
|
const handleDropEvent = (event, date, time = null) => {
|
|
|
|
|
const type = event.dataTransfer.getData('type');
|
|
|
|
|
const data = JSON.parse(event.dataTransfer.getData('data'));
|
|
|
|
|
|
|
|
|
|
if (type === 'common') {
|
|
|
|
|
// 添加新日程
|
|
|
|
|
const newEvent = {
|
|
|
|
|
id: 'event-' + Date.now(),
|
|
|
|
|
title: data.title,
|
|
|
|
|
date: date,
|
|
|
|
|
startTime: time || '09:00',
|
|
|
|
|
duration: data.duration,
|
|
|
|
|
location: '',
|
|
|
|
|
color: data.color,
|
|
|
|
|
creator: {
|
|
|
|
|
id: user.value.id,
|
|
|
|
|
name: user.value.name
|
|
|
|
|
},
|
|
|
|
|
participants: [{
|
|
|
|
|
id: user.value.id,
|
|
|
|
|
name: user.value.name
|
|
|
|
|
}],
|
|
|
|
|
completed: false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
events.value.push(newEvent);
|
|
|
|
|
} else if (type === 'event') {
|
|
|
|
|
// 移动现有日程
|
|
|
|
|
const eventToUpdate = events.value.find(e => e.id === data.id);
|
|
|
|
|
if (eventToUpdate) {
|
|
|
|
|
eventToUpdate.date = date;
|
|
|
|
|
if (time) {
|
|
|
|
|
eventToUpdate.startTime = time;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localStorage.setItem('events', JSON.stringify(events.value));
|
|
|
|
|
|
|
|
|
|
// 移除拖拽样式
|
|
|
|
|
document.querySelectorAll('.dropzone').forEach(el => {
|
|
|
|
|
el.classList.remove('drag-over');
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理登录
|
|
|
|
|
const handleLogin = (userData) => {
|
|
|
|
|
user.value = {
|
|
|
|
|
...userData,
|
|
|
|
|
team: '萧康云医技术开发团队-医生组'
|
|
|
|
|
};
|
|
|
|
|
showLoginModal.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
const savedEvents = localStorage.getItem('events');
|
|
|
|
|
if (savedEvents) {
|
|
|
|
|
events.value = JSON.parse(savedEvents);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const savedCommonEvents = localStorage.getItem('commonEvents');
|
|
|
|
|
if (savedCommonEvents) {
|
|
|
|
|
commonEvents.value = JSON.parse(savedCommonEvents);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-04-24 13:39:24 +08:00
|
|
|
</script>
|
|
|
|
|
|
2025-06-10 17:03:02 +08:00
|
|
|
<style>
|
|
|
|
|
.dragging {
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dropzone.drag-over {
|
|
|
|
|
animation: pulse 1.5s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
0% {
|
|
|
|
|
box-shadow: inset 0 0 0 0 rgba(100, 94, 255, 0.4);
|
|
|
|
|
}
|
|
|
|
|
70% {
|
|
|
|
|
box-shadow: inset 0 0 0 10px rgba(100, 94, 255, 0);
|
|
|
|
|
}
|
|
|
|
|
100% {
|
|
|
|
|
box-shadow: inset 0 0 0 0 rgba(100, 94, 255, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 按钮悬停效果 */
|
|
|
|
|
button {
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
2025-04-24 13:39:24 +08:00
|
|
|
|
2025-06-10 17:03:02 +08:00
|
|
|
button:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
|
|
|
}
|
2025-05-15 21:24:56 +08:00
|
|
|
|
2025-06-10 17:03:02 +08:00
|
|
|
button:active {
|
|
|
|
|
transform: translateY(1px);
|
|
|
|
|
}
|
|
|
|
|
</style>
|