This commit is contained in:
2025-06-10 22:04:23 +08:00
parent c0b62dfa0f
commit df13a5b6c3
11 changed files with 652 additions and 516 deletions

View File

@@ -18,6 +18,7 @@
"aos": "^2.3.4",
"axios": "^1.6.2",
"chinese-lunar": "^0.1.4",
"chinese-lunar-calendar": "^1.0.1",
"dayjs": "^1.11.13",
"lucide-vue-next": "^0.507.0",
"pinia": "^2.1.7",

33
pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ importers:
chinese-lunar:
specifier: ^0.1.4
version: 0.1.4
chinese-lunar-calendar:
specifier: ^1.0.1
version: 1.0.1
dayjs:
specifier: ^1.11.13
version: 1.11.13
@@ -308,36 +311,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -395,56 +404,67 @@ packages:
resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.40.0':
resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.40.0':
resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.40.0':
resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.40.0':
resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.40.0':
resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.40.0':
resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.40.0':
resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.40.0':
resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.40.0':
resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.40.0':
resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
@@ -502,24 +522,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.4':
resolution: {integrity: sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.4':
resolution: {integrity: sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.4':
resolution: {integrity: sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.4':
resolution: {integrity: sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==}
@@ -647,6 +671,9 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
chinese-lunar-calendar@1.0.1:
resolution: {integrity: sha512-T5XjtA6ygOkxjwltYf8dEB1DLsPKewDKajf74fNeT6rH2g9EZPf2AHk6vAlK/SAImTYKXoUNQ3Q4JwN0BduqRA==}
chinese-lunar@0.1.4:
resolution: {integrity: sha512-E4seb9UZkSJtkm65Ry2ZLWTw5Z87DOyGcCePdg3ft7da342hnEliMtcDAPSn+YSo/o0xl1Egmhh671kxEJgY+g==}
@@ -846,24 +873,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.29.2:
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.29.2:
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.29.2:
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.29.2:
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
@@ -1570,6 +1601,8 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
chinese-lunar-calendar@1.0.1: {}
chinese-lunar@0.1.4: {}
chokidar@4.0.3:

View File

@@ -1,17 +1,17 @@
<template>
<div class="min-h-screen w-full bg-gradient-to-br from-gray-50 to-gray-100">
<div class="min-h-screen w-full bg-gradient-to-br from-gray-50 to-gray-100 bg-fixed">
<!-- 登录模态框 -->
<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">
<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 w-full">
<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>
<i class="fas fa-calendar-alt text-white text-2xl"></i>
</div>
<h1 class="ml-4 text-2xl font-bold text-b drop-shadow">智能日程管家</h1>
<h1 class="ml-4 text-2xl font-bold text-white drop-shadow">智能日程管家</h1>
</div>
<div class="mt-4 sm:mt-0 flex items-center">
@@ -50,13 +50,13 @@
</div>
</header>
<div class="max-w-7xl mx-auto px-4 py-8 sm:px-6">
<div class="w-full 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"
class="w-10 h-10 flex items-center justify-center rounded-full bg-white hover:bg-gray-100 transition-all shadow hover:scale-105 active:scale-95"
>
<i class="fas fa-chevron-left text-primary-600"></i>
</button>
@@ -71,7 +71,7 @@
<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"
class="w-10 h-10 flex items-center justify-center rounded-full bg-white hover:bg-gray-100 transition-all shadow hover:scale-105 active:scale-95"
>
<i class="fas fa-chevron-right text-primary-600"></i>
</button>
@@ -79,14 +79,14 @@
<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"
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 active:translate-y-0"
>
<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="grid grid-cols-1 lg:grid-cols-[1fr_2fr_1fr] gap-6">
<!-- 左侧面板 -->
<div class="lg:col-span-1" data-aos="fade-right" data-aos-delay="100">
<CommonEvents
@@ -98,7 +98,7 @@
</div>
<!-- 中间面板 - 日历 -->
<div class="lg:col-span-2" data-aos="fade-up" data-aos-delay="200">
<div class="lg:col-span-1" data-aos="fade-up" data-aos-delay="200">
<CalendarView
:view="view"
:currentDate="currentDate"
@@ -132,8 +132,9 @@
<!-- 模态框 -->
<EventModal
v-if="showEventModal"
:event="editingEvent || newEvent"
:event="editingEvent"
:isEditing="!!editingEvent"
:isOpen="showEventModal"
@close="closeModal"
@save="saveEvent"
@delete="deleteEvent"
@@ -141,6 +142,7 @@
<CommonEventModal
v-if="showCommonEventModal"
:isOpen="showCommonEventModal"
@close="closeCommonModal"
@save="saveCommonEvent"
/>
@@ -157,6 +159,8 @@ import CommonEvents from './components/CommonEvents.vue';
import EventModal from './components/EventModal.vue';
import CommonEventModal from './components/CommonEventModal.vue';
import LoginModal from './components/LoginModal.vue';
import AOS from 'aos';
import 'aos/dist/aos.css';
dayjs.locale('zh-cn');
@@ -235,20 +239,6 @@ const commonEvents = ref([
}
]);
// 新事件模板
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') {
@@ -279,24 +269,11 @@ const changeDate = (offset) => {
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 = () => {
@@ -430,6 +407,13 @@ const handleLogin = (userData) => {
// 初始化
onMounted(() => {
AOS.init({
duration: 800,
easing: 'ease-out-quart',
once: true,
mirror: false
});
const savedEvents = localStorage.getItem('events');
if (savedEvents) {
events.value = JSON.parse(savedEvents);
@@ -439,6 +423,13 @@ onMounted(() => {
if (savedCommonEvents) {
commonEvents.value = JSON.parse(savedCommonEvents);
}
const savedUser = localStorage.getItem('user');
if (savedUser) {
console.log(savedUser, 'ssssssssss')
user.value = JSON.parse(savedUser);
showLoginModal.value = false;
}
});
</script>
@@ -478,4 +469,4 @@ button:hover {
button:active {
transform: translateY(1px);
}
</style>
</style>

View File

@@ -1,7 +1,7 @@
<template>
<div class="bg-white rounded-2xl shadow-soft p-6">
<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">
<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 ['日', '一', '二', '三', '四', '五', '六']"
@@ -20,8 +20,11 @@
@dragover.prevent="handleDragOver($event, day.date)"
@dragleave="handleDragLeave"
@drop="handleDropEvent($event, day.date)"
:class="{'bg-primary-50': selectedDate === day.date}"
class="calendar-cell dropzone rounded-xl border border-gray-100 p-3 cursor-pointer transition-all relative min-h-[120px]"
:class="{
'bg-primary-50': 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">
@@ -59,25 +62,28 @@
</div>
<!-- 事件展示 -->
<div v-if="hasEvent(day.date)" class="mt-2 text-xs truncate max-w-full">
{{ getFirstEventTitle(day.date) }}
</div>
<!-- 事件指示器 -->
<div v-if="hasEvent(day.date)" class="absolute bottom-3 right-3 flex">
<span
v-for="(color, index) in getEventColorsForDay(day.date)"
:key="index"
class="event-dot"
:style="{backgroundColor: color}"
></span>
<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">
<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
@@ -85,7 +91,7 @@
: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"
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
@@ -120,7 +126,8 @@
@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
'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"
>
@@ -128,7 +135,7 @@
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"
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}`
@@ -142,7 +149,7 @@
</div>
<!-- 日视图 -->
<div v-else class="day-view">
<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)"
@@ -175,6 +182,9 @@
@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
@@ -183,7 +193,7 @@
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"
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
@@ -205,7 +215,7 @@
</template>
<script setup>
import { computed, defineProps, defineEmits } from 'vue';
import { computed, defineProps, defineEmits, ref } from 'vue';
import dayjs from 'dayjs';
import { getLunarDate, getFestival, getSolarTerm, isHoliday } from '@/utils/calendarUtils';
@@ -219,6 +229,9 @@ const props = defineProps({
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');
@@ -257,16 +270,15 @@ const hasEvent = (date) => {
return props.events.some(e => e.date === date);
};
// 获取当天第一个事件的标题
const getFirstEventTitle = (date) => {
const events = props.events.filter(e => e.date === date);
return events.length > 0 ? events[0].title : '';
};
// 获取当天的所有事件
const getEventsForDate = (date, time = null) => {
let filteredEvents = props.events.filter(e => e.date === date);
// 获取当天所有事件颜色
const getEventColorsForDay = (date) => {
const events = props.events.filter(e => e.date === date);
return [...new Set(events.map(e => e.color))];
if (time) {
filteredEvents = filteredEvents.filter(e => e.startTime === time);
}
return filteredEvents;
};
// 检查是否是今天
@@ -279,17 +291,6 @@ const isWorkDayOnWeekend = (date) => {
return false;
};
// 获取指定时间的日程
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 selectDate = (date) => {
emit('select-date', date);
};
@@ -310,17 +311,23 @@ const handleDragEnd = () => {
// 处理拖拽悬停
const handleDragOver = (event, date, time = null) => {
event.currentTarget.classList.add('drag-over');
// 生成唯一标识,用于标记当前拖拽悬停的单元格
if (time) {
isDragOverCell.value = `${date}-${time}`;
} else {
isDragOverCell.value = date;
}
event.preventDefault();
};
// 处理拖拽离开
const handleDragLeave = (event) => {
event.currentTarget.classList.remove('drag-over');
isDragOverCell.value = null;
};
// 处理放置事件
const handleDropEvent = (event, date, time = null) => {
event.currentTarget.classList.remove('drag-over');
isDragOverCell.value = null;
emit('drop-event', event, date, time);
};
</script>
@@ -400,4 +407,4 @@ const handleDropEvent = (event, date, time = null) => {
.day-header {
background: linear-gradient(to right, #f0f4ff, #e0e7ff);
}
</style>
</style>

View File

@@ -1,101 +1,141 @@
<template>
<transition name="modal">
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 px-4 py-6 backdrop-blur-sm">
<div class="modal-container bg-white rounded-2xl w-full max-w-md mx-auto shadow-soft-lg transition-all duration-300 transform scale-100">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<h3 class="text-lg font-bold text-gray-800">添加快捷日程</h3>
<button @click="$emit('close')" class="w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center transition-colors">
<i class="fas fa-times text-gray-600"></i>
</button>
</div>
<div class="p-6">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">标题</label>
<input
v-model="eventData.title"
type="text"
placeholder="快捷日程名称"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors"
:class="{'border-red-500': !eventData.title}"
>
<p v-if="!eventData.title" class="text-red-500 text-xs mt-1">标题不能为空</p>
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">默认持续时间</label>
<div class="relative">
<input
v-model="eventData.duration"
type="number"
min="5"
max="240"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors pr-12"
:class="{'border-red-500': !eventData.duration}"
>
<span class="absolute inset-y-0 right-4 flex items-center text-gray-500">分钟</span>
<p v-if="!eventData.duration" class="text-red-500 text-xs mt-1">请填写持续时间</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">选择颜色</label>
<div class="flex gap-2">
<button
v-for="color in colors"
:key="color"
@click="eventData.color = color"
:class="{'ring-2 ring-offset-2 ring-gray-800': eventData.color === color}"
class="w-8 h-8 rounded-full focus:outline-none transition-all hover:scale-110"
:style="{ backgroundColor: color }"
></button>
</div>
</div>
</div>
<div class="flex justify-end space-x-3">
<button
@click="save"
class="px-5 py-2.5 text-sm rounded-lg bg-gradient-to-r from-secondary-500 to-secondary-700 hover:from-secondary-600 hover:to-secondary-800 text-white transition-colors font-medium hover:shadow-md"
>
添加快捷日程
<transition
enter-active-class="transition-all duration-300 ease-out"
leave-active-class="transition-all duration-200 ease-in"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="isOpen" class="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 px-4 py-6">
<transition
enter-active-class="transition-all duration-300 ease-out"
leave-active-class="transition-all duration-200 ease-in"
enter-from-class="opacity-0 translate-y-4"
enter-to-class="opacity-100 translate-y-0"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-4"
>
<div v-if="isOpen" class="modal-container bg-white rounded-2xl w-full max-w-md mx-auto shadow-2xl transition-all duration-300 transform">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<h3 class="text-lg font-bold text-gray-800">添加快捷日程</h3>
<button @click="$emit('close')" class="w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center transition-colors hover:scale-110 active:scale-95">
<i class="fas fa-times text-gray-600"></i>
</button>
</div>
<div class="p-6">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
标题 <span class="text-red-500">*</span>
</label>
<input
v-model="eventData.title"
type="text"
placeholder="快捷日程名称"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
:class="{'border-red-500 ring-2 ring-red-200': !eventData.title && touched.title}"
@blur="touched.title = true"
>
<p v-if="!eventData.title && touched.title" class="text-red-500 text-xs mt-1 animate-pulse">
<i class="fas fa-exclamation-circle mr-1"></i> 标题不能为空
</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
描述
</label>
<textarea
v-model="eventData.description"
placeholder="可选:添加日程描述"
rows="3"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
></textarea>
</div>
<div class="grid grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
默认持续时间 <span class="text-red-500">*</span>
</label>
<div class="relative">
<input
v-model="eventData.duration"
type="number"
min="5"
max="240"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200 pr-12"
:class="{'border-red-500 ring-2 ring-red-200': !eventData.duration && touched.duration}"
@blur="touched.duration = true"
>
<span class="absolute inset-y-0 right-4 flex items-center text-gray-500">分钟</span>
<p v-if="!eventData.duration && touched.duration" class="text-red-500 text-xs mt-1 animate-pulse">
<i class="fas fa-exclamation-circle mr-1"></i> 请填写持续时间
</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">选择颜色</label>
<div class="flex gap-2">
<button
v-for="color in colors"
:key="color"
@click="eventData.color = color"
:class="{'ring-2 ring-offset-2 ring-gray-800': eventData.color === color}"
class="w-8 h-8 rounded-full focus:outline-none transition-all hover:scale-110 active:scale-95"
:style="{ backgroundColor: color }"
></button>
</div>
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
重复设置
</label>
<select
v-model="eventData.repeat"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
>
<option value="">不重复</option>
<option value="daily">每天</option>
<option value="weekly">每周</option>
<option value="monthly">每月</option>
</select>
</div>
<div class="flex justify-end space-x-3">
<button
@click="$emit('close')"
class="px-5 py-2.5 text-sm rounded-lg border border-gray-300 hover:bg-gray-50 text-gray-700 transition-colors font-medium hover:shadow-md hover:-translate-y-0.5 active:translate-y-0"
>
取消
</button>
<button
@click="save"
class="px-5 py-2.5 text-sm rounded-lg bg-gradient-to-r from-secondary-500 to-secondary-700 hover:from-secondary-600 hover:to-secondary-800 text-white transition-colors font-medium hover:shadow-md hover:-translate-y-0.5 active:translate-y-0"
>
添加快捷日程
</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
isOpen: Boolean
});
const emit = defineEmits(['close', 'save']);
const colors = [
@@ -108,32 +148,37 @@ const colors = [
const eventData = ref({
title: '',
description: '',
duration: 30,
color: colors[1]
color: colors[1],
repeat: ''
});
const touched = ref({
title: false,
duration: false
});
const save = () => {
if (!eventData.value.title.trim()) {
// 标记字段为已触摸
touched.value.title = true;
touched.value.duration = true;
if (!eventData.value.title.trim() || !eventData.value.duration) {
return;
}
emit('save', eventData.value);
};
</script>
<style scoped>
/* 模态框动画 */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-container {
transition: all 0.3s ease;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style>
button {
transition: all 0.3s ease;
cursor: pointer;
}
</style>

View File

@@ -1,13 +1,12 @@
<template>
<div>
<div data-aos="zoom-in" data-aos-duration="500">
<!-- 说明卡片 -->
<div class="bg-white rounded-2xl shadow-soft overflow-hidden mb-6" data-aos="zoom-in">
<div class="bg-gradient-to-br from-white to-gray-50 rounded-2xl shadow-soft overflow-hidden mb-6">
<div class="bg-gradient-to-r from-primary-500 to-primary-700 px-6 py-4">
<h2 class="text-white font-bold text-lg flex items-center">
<i class="fas fa-info-circle mr-2"></i>
操作指南
</h2>
</div>
<div class="p-6">
@@ -16,69 +15,50 @@
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-100 text-primary-700 flex items-center justify-center text-xs mr-3">
1
</div>
<div>
<p class="font-medium">拖放快捷日程到日历</p>
<p class="text-sm text-gray-600 mt-1">将左侧的快捷日程拖放到日历中的日期添加新日程</p>
</div>
</li>
<li class="flex">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-100 text-primary-700 flex items-center justify-center text-xs mr-3">
2
</div>
<div>
<p class="font-medium">拖放日程到其他日期</p>
<p class="text-sm text-gray-600 mt-1">将右侧详情中的日程拖放到日历中的其他日期进行移动</p>
</div>
</li>
<li class="flex">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary-100 text-primary-700 flex items-center justify-center text-xs mr-3">
3
</div>
<div>
<p class="font-medium">自定义快捷日程</p>
<p class="text-sm text-gray-600 mt-1">点击底部按钮添加快捷日程模板</p>
</div>
</li>
</ul>
</div>
</div>
<!-- 常用日程卡片 -->
<div class="bg-white rounded-2xl shadow-soft overflow-hidden" data-aos="zoom-in" data-aos-delay="100">
<div class="bg-gradient-to-br from-white to-gray-50 rounded-2xl shadow-soft overflow-hidden" data-aos="zoom-in" data-aos-delay="100">
<div class="flex justify-between items-center bg-gradient-to-r from-secondary-500 to-secondary-700 px-6 py-4">
<h2 class="text-white font-bold text-lg flex items-center">
<i class="fas fa-star mr-2"></i>
常用日程
<span class="ml-2 text-sm bg-white text-secondary-700 px-2 py-1 rounded-md">可拖拽</span>
</h2>
<button
@click="$emit('add-common-event')"
class="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center transition-colors hover:bg-white/30 hover:scale-110"
class="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center transition-colors hover:bg-white/30 hover:scale-110 hover:shadow-sm active:scale-95"
>
<i class="fas fa-plus text-white text-sm"></i>
</button>
</div>
<div class="p-5 max-h-[420px] overflow-y-auto">
@@ -98,72 +78,51 @@
<div class="ml-4 flex-1">
<h3 class="font-medium text-gray-800">{{ event.title }}</h3>
<div class="flex items-center mt-1 text-xs text-gray-500">
<span>持续: {{ event.duration }}分钟</span>
<span class="mx-2">|</span>
<span>类型: 快捷</span>
</div>
</div>
<div class="flex">
<span class="text-gray-400 group-hover:text-primary-600 transition-colors">
<i class="fas fa-grip-lines"></i>
</span>
</div>
</div>
</div>
<div v-if="commonEvents.length === 0" class="py-8 text-center text-gray-500">
<div
class="bg-gradient-to-br from-gray-100 to-gray-200 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4"
>
<i class="fas fa-inbox text-2xl text-gray-400"></i>
</div>
<p>暂无快捷日程</p>
<button
@click="$emit('add-common-event')"
class="mt-4 inline-flex items-center px-4 py-2 bg-gradient-to-r from-secondary-500 to-secondary-700 hover:from-secondary-600 hover:to-secondary-800 text-white rounded-xl transition-all hover:shadow-md"
class="mt-4 inline-flex items-center px-4 py-2 bg-gradient-to-r from-secondary-500 to-secondary-700 hover:from-secondary-600 hover:to-secondary-800 text-white rounded-xl transition-all hover:shadow-md hover:-translate-y-0.5 active:translate-y-0"
>
<i class="fas fa-plus mr-2"></i>
添加快捷日程
</button>
</div>
<div class="mt-4">
<button
@click="$emit('add-common-event')"
class="w-full flex items-center justify-center py-3 bg-gray-50 hover:bg-gray-100 rounded-xl text-secondary-600 font-medium transition-colors hover:shadow-sm"
class="w-full flex items-center justify-center py-3 bg-gray-50 hover:bg-gray-100 rounded-xl text-secondary-600 font-medium transition-colors hover:shadow-sm hover:-translate-y-0.5 active:translate-y-0"
>
<i class="fas fa-plus-circle mr-2"></i>
新增快捷日程
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
@@ -182,7 +141,6 @@ const handleDragEnd = () => {
};
</script>
<style scoped>
.common-event {
transition: all 0.3s ease;
@@ -191,5 +149,11 @@ const handleDragEnd = () => {
.common-event:hover {
transform: translateX(5px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
button {
transition: all 0.3s ease;
cursor: pointer;
}
</style>

View File

@@ -1,10 +1,13 @@
<template>
<div
class="bg-white rounded-2xl shadow-soft p-6 h-full"
class="bg-gradient-to-br from-white to-gray-50 rounded-2xl shadow-soft p-6 h-full"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
:class="{'drag-over': isDragOver}"
data-aos="fade-left"
data-aos-duration="500"
data-aos-delay="300"
>
<div
v-if="!selectedDate"
@@ -12,29 +15,20 @@
data-aos="fade-in"
>
<div
class="bg-gradient-to-br from-gray-200 to-gray-300 w-24 h-24 rounded-full flex items-center justify-center mx-auto mb-5">
<i class="fas fa-calendar-star text-4xl text-gray-600"></i>
class="bg-gradient-to-br from-gray-100 to-gray-200 w-24 h-24 rounded-full flex items-center justify-center mx-auto mb-5">
<i class="fas fa-calendar-star text-4xl text-gray-400"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-2">选择日期查看详情</h3>
<p class="text-gray-600 mb-6">点击日历中的日期查看该日的日程详情</p>
<div class="flex">
<span class="event-dot bg-primary-600"></span>
<span class="event-dot bg-secondary-500 ml-1"></span>
<span class="event-dot bg-accent-500 ml-1"></span>
</div>
<p class="text-gray-500 mt-4">将快捷日程拖到此处添加到选中日期</p>
</div>
<div v-if="selectedDate" class="h-full flex flex-col">
<div class="pb-4 border-b border-gray-100" data-aos="fade-in">
<div class="flex justify-between">
@@ -43,46 +37,36 @@
<span class="text-lg text-primary-600 ml-2">
{{ dayjs(selectedDate).format('dddd') }}
</span>
</h2>
<button
@click="$emit('add-event')"
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-md hover:shadow-lg"
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-md hover:shadow-lg hover:-translate-y-0.5 active:translate-y-0"
>
<i class="fas fa-plus mr-2"></i>
添加日程
</button>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<span v-if="getFestival(selectedDate)" class="px-3 py-1 bg-red-100 text-red-600 rounded-full text-sm">
<i class="fas fa-gift mr-1"></i>
{{ getFestival(selectedDate) }}
<i class="fas fa-gift mr-1"></i> {{ getFestival(selectedDate) }}
</span>
<span v-if="getSolarTerm(selectedDate)" class="px-3 py-1 bg-green-100 text-green-600 rounded-full text-sm">
<i class="fas fa-leaf mr-1"></i>
{{ getSolarTerm(selectedDate) }}
<i class="fas fa-leaf mr-1"></i> {{ getSolarTerm(selectedDate) }}
</span>
<span v-if="isHoliday(selectedDate)" class="px-3 py-1 bg-yellow-100 text-yellow-600 rounded-full text-sm">
<i class="fas fa-umbrella-beach mr-1"></i>
休息日
<i class="fas fa-umbrella-beach mr-1"></i> 休息日
</span>
<span v-else class="px-3 py-1 bg-blue-100 text-blue-600 rounded-full text-sm">
<i class="fas fa-briefcase mr-1"></i>
工作日
<i class="fas fa-briefcase mr-1"></i> 工作日
</span>
</div>
</div>
<div class="mt-4 flex-1 overflow-y-auto">
<div class="flex justify-between items-center mb-3">
<h3 class="font-semibold text-gray-800 flex items-center">
@@ -93,16 +77,14 @@
<span class="text-sm text-gray-500">
{{ getEventsForDate(selectedDate).length }} 个日程
</span>
</div>
<div
v-if="getEventsForDate(selectedDate).length > 0"
class="space-y-3"
>
<div
v-for="event in getEventsForDate(selectedDate)"
v-for="(event, index) in getEventsForDate(selectedDate)"
:key="event.id"
draggable="true"
@dragstart="handleDragStart($event, event)"
@@ -119,31 +101,23 @@
<div class="flex-1">
<div class="flex justify-between">
<h4 class="font-semibold text-gray-800">{{ event.title }}</h4>
<div>
<button
@click.stop="$emit('edit-event', event)"
class="w-8 h-8 flex items-center justify-center rounded-full bg-white hover:bg-gray-100 text-gray-500 hover:text-primary-600"
class="w-8 h-8 flex items-center justify-center rounded-full bg-white hover:bg-gray-100 text-gray-500 hover:text-primary-600 hover:scale-110 active:scale-95"
>
<i class="fas fa-edit"></i>
</button>
</div>
</div>
<div class="mt-2 flex flex-wrap gap-2">
<span class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-white shadow-sm">
<i class="fas fa-clock mr-1" :style="{ color: event.color }"></i>
<span :style="{ color: event.color }">
{{ event.startTime }}
<span v-if="event.duration">({{ event.duration }}分钟)</span>
</span>
</span>
<span
@@ -151,102 +125,77 @@
class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-white shadow-sm"
>
<i class="fas fa-location-dot mr-1 text-gray-500"></i>
{{ event.location }}
</span>
</div>
<!-- 新增字段 -->
<div class="mt-4 grid grid-cols-2 gap-3">
<div>
<p class="text-sm text-gray-500">发起人</p>
<p class="font-medium flex items-center">
<i class="fas fa-user-circle mr-2 text-primary-500"></i>
{{ event.creator?.name || '测试' }}
{{ event.creator?.name || '未设置' }}
</p>
</div>
<div>
<p class="text-sm text-gray-500">状态</p>
<div>
<span v-if="event.completed" class="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs">
已完成
</span>
<span v-else class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded-full text-xs">
进行中
</span>
</div>
</div>
</div>
<div class="mt-3">
<p class="text-sm text-gray-500">参与人</p>
<div class="flex flex-wrap mt-1">
<div v-for="(participant, index) in event.participants" :key="index"
class="flex items-center mr-3 mb-2">
<div v-for="(participant, pIndex) in event.participants" :key="pIndex"
class="flex items-center mr-3 mb-2 bg-gray-100 px-2 py-1 rounded-full">
<i class="fas fa-user mr-1 text-sm text-gray-500"></i>
<span class="text-sm">{{ participant.name }}</span>
</div>
<div v-if="!event.participants || event.participants.length === 0" class="text-gray-400 text-xs">
暂无参与人
</div>
</div>
</div>
<div v-if="event.description" class="mt-3 text-sm text-gray-600">
<p class="font-medium text-gray-700 mb-1">描述</p>
<p class="whitespace-pre-line">{{ event.description }}</p>
</div>
</div>
</div>
</div>
</div>
<div v-else class="py-8 text-center text-gray-500" data-aos="fade-in">
<div
class="bg-gradient-to-br from-gray-100 to-gray-200 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4"
>
<i class="fas fa-calendar-plus text-2xl text-gray-400"></i>
</div>
<p>今日暂无日程安排</p>
<p class="text-sm mt-2 text-gray-500 mb-4">点击"添加日程"或拖放快捷日程创建计划</p>
<button
@click="$emit('add-event')"
class="inline-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-md hover:shadow-lg"
class="inline-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-md hover:shadow-lg hover:-translate-y-0.5 active:translate-y-0"
>
<i class="fas fa-plus mr-2"></i>
添加新日程
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref } from 'vue';
import dayjs from 'dayjs';
@@ -331,4 +280,9 @@ const handleDrop = (event) => {
box-shadow: inset 0 0 0 0 rgba(100, 94, 255, 0);
}
}
</style>
button {
transition: all 0.3s ease;
cursor: pointer;
}
</style>

View File

@@ -1,174 +1,216 @@
<template>
<transition name="modal">
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 px-4 py-6 backdrop-blur-sm">
<div class="modal-container bg-white rounded-2xl w-full max-w-md mx-auto shadow-soft-lg transition-all duration-300 transform scale-100">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<h3 class="text-lg font-bold text-gray-800">
{{ isEditing ? '编辑日程' : '添加新日程' }}
</h3>
<button @click="$emit('close')" class="w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center transition-colors">
<i class="fas fa-times text-gray-600"></i>
</button>
</div>
<div class="p-6">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">标题</label>
<input
v-model="eventData.title"
type="text"
placeholder="日程标题"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors"
:class="{'border-red-500': !eventData.title}"
>
<p v-if="!eventData.title" class="text-red-500 text-xs mt-1">标题不能为空</p>
<!-- 外层背景动画 -->
<transition
enter-active-class="transition-all duration-300 ease-out"
leave-active-class="transition-all duration-200 ease-in"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="isOpen" class="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-50 px-4 py-6">
<!-- 内层模态框动画 -->
<transition
enter-active-class="transition-all duration-300 ease-out"
leave-active-class="transition-all duration-200 ease-in"
enter-from-class="opacity-0 translate-y-4"
enter-to-class="opacity-100 translate-y-0"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-4"
>
<div class="modal-container bg-white rounded-2xl w-full max-w-md mx-auto shadow-soft-lg overflow-hidden">
<!-- 模态框头部 -->
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gradient-to-r from-gray-50 to-white">
<h3 class="text-lg font-bold text-gray-800 flex items-center">
<i class="fas fa-calendar-plus text-primary-500 mr-2"></i>
{{ isEditing ? '编辑日程' : '添加新日程' }}
</h3>
<button @click="$emit('close')" class="w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center transition-colors hover:scale-110 active:scale-95">
<i class="fas fa-times text-gray-600"></i>
</button>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">日期</label>
<!-- 模态框主体内容 -->
<div class="p-6 overflow-y-auto max-h-[80vh]">
<!-- 标题输入 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
标题 <span class="text-red-500">*</span>
</label>
<input
v-model="eventData.date"
type="date"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">时间</label>
<input
v-model="eventData.startTime"
type="time"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors"
>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">地点</label>
<input
v-model="eventData.location"
v-model="eventData.title"
type="text"
placeholder="如:会议室"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors"
placeholder="日程标题"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
:class="{'border-red-500 ring-2 ring-red-200': !eventData.title && touched.title}"
@blur="touched.title = true"
>
<p v-if="!eventData.title && touched.title" class="text-red-500 text-xs mt-1 animate-pulse">
<i class="fas fa-exclamation-circle mr-1"></i> 标题不能为空
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">持续时间</label>
<div class="relative">
<!-- 日期和时间 -->
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
日期 <span class="text-red-500">*</span>
</label>
<input
v-model="eventData.duration"
type="number"
min="5"
max="480"
v-model="eventData.date"
type="date"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors pr-12"
:class="{'border-red-500': !eventData.duration}"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
>
<span class="absolute inset-y-0 right-4 flex items-center text-gray-500">分钟</span>
<p v-if="!eventData.duration" class="text-red-500 text-xs mt-1">请填写持续时间</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
时间 <span class="text-red-500">*</span>
</label>
<input
v-model="eventData.startTime"
type="time"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
>
</div>
</div>
</div>
<!-- 新增字段 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
<div class="flex space-x-4">
<label class="flex items-center">
<input type="radio" v-model="eventData.completed" :value="false" class="text-primary-600">
<span class="ml-2">进行中</span>
</label>
<label class="flex items-center">
<input type="radio" v-model="eventData.completed" :value="true" class="text-primary-600">
<span class="ml-2">已完成</span>
</label>
<!-- 地点和持续时间 -->
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">地点</label>
<input
v-model="eventData.location"
type="text"
placeholder="如:会议室"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
持续时间 <span class="text-red-500">*</span>
</label>
<div class="relative">
<input
v-model="eventData.duration"
type="number"
min="5"
max="480"
required
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200 pr-12"
:class="{'border-red-500 ring-2 ring-red-200': !eventData.duration && touched.duration}"
@blur="touched.duration = true"
>
<span class="absolute inset-y-0 right-4 flex items-center text-gray-500">分钟</span>
<p v-if="!eventData.duration && touched.duration" class="text-red-500 text-xs mt-1 animate-pulse">
<i class="fas fa-exclamation-circle mr-1"></i> 请填写持续时间
</p>
</div>
</div>
</div>
</div>
<!-- 描述 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">描述</label>
<textarea
v-model="eventData.description"
rows="3"
placeholder="输入日程详细描述..."
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
></textarea>
</div>
<!-- 参与人 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">参与人</label>
<div class="flex flex-wrap gap-2">
<div v-for="(participant, index) in eventData.participants" :key="index"
class="flex items-center bg-gray-100 px-3 py-1 rounded-full">
<input
v-model="participant.name"
type="text"
placeholder="姓名"
class="bg-transparent outline-none text-sm w-20"
>
<button @click="removeParticipant(index)" class="ml-1 text-gray-400 hover:text-red-500">
<i class="fas fa-times text-xs"></i>
</button>
</div>
<button
@click="addParticipant"
class="w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 text-gray-500 hover:text-primary-600"
>
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">选择颜色</label>
<!-- 状态 -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
<div class="flex space-x-4">
<label class="flex items-center">
<input type="radio" v-model="eventData.completed" :value="false" class="text-primary-600 focus:ring-primary-500">
<span class="ml-2">进行中</span>
</label>
<label class="flex items-center">
<input type="radio" v-model="eventData.completed" :value="true" class="text-primary-600 focus:ring-primary-500">
<span class="ml-2">已完成</span>
</label>
</div>
</div>
<div class="flex gap-2">
<!-- 颜色选择 -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">选择颜色</label>
<div class="flex gap-2">
<button
v-for="color in colors"
:key="color"
@click="eventData.color = color"
:class="{'ring-2 ring-offset-2 ring-gray-800': eventData.color === color}"
class="w-8 h-8 rounded-full focus:outline-none transition-all hover:scale-110 active:scale-95"
:style="{ backgroundColor: color }"
></button>
</div>
</div>
<!-- 底部按钮 -->
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-100">
<button
v-for="color in colors"
:key="color"
@click="eventData.color = color"
:class="{'ring-2 ring-offset-2 ring-gray-800': eventData.color === color}"
class="w-8 h-8 rounded-full focus:outline-none transition-all hover:scale-110"
:style="{ backgroundColor: color }"
></button>
@click="$emit('delete')"
v-if="isEditing"
class="px-4 py-2 text-sm rounded-lg border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 flex items-center"
>
<i class="fas fa-trash-alt mr-2"></i> 删除
</button>
<button
@click="save"
class="px-5 py-2.5 text-sm rounded-lg bg-gradient-to-r from-primary-500 to-primary-700 hover:from-primary-600 hover:to-primary-800 text-white transition-colors font-medium hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 flex items-center"
>
<i class="fas fa-save mr-2"></i>
{{ isEditing ? '更新日程' : '添加日程' }}
</button>
</div>
</div>
<div class="flex justify-end space-x-3">
<button
@click="$emit('delete')"
v-if="isEditing"
class="px-4 py-2 text-sm rounded-lg border border-gray-200 text-gray-700 hover:bg-gray-50 transition-colors"
>
删除
</button>
<button
@click="save"
class="px-5 py-2.5 text-sm rounded-lg bg-gradient-to-r from-primary-500 to-primary-700 hover:from-primary-600 hover:to-primary-800 text-white transition-colors font-medium hover:shadow-md"
>
{{ isEditing ? '更新日程' : '添加日程' }}
</button>
</div>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
event: Object,
isEditing: Boolean
isEditing: Boolean,
isOpen: Boolean
});
const emit = defineEmits(['close', 'save', 'delete']);
@@ -181,35 +223,125 @@ const colors = [
'#8c6bed'
];
const eventData = ref({ ...props.event });
const defaultEventData = {
title: '',
date: '',
startTime: '10:00',
duration: 30,
location: '',
description: '',
color: colors[0],
completed: false,
participants: []
};
const eventData = ref({
...defaultEventData,
...(props.event || {})
});
const touched = ref({
title: false,
duration: false
});
// 添加参与人
const addParticipant = () => {
if (!eventData.value.participants) {
eventData.value.participants = [];
}
eventData.value.participants.push({ name: '' });
};
// 移除参与人
const removeParticipant = (index) => {
eventData.value.participants.splice(index, 1);
};
// 当传入的 event prop 变化时更新本地值
watch(() => props.event, (newValue) => {
eventData.value = { ...newValue };
});
eventData.value = {
...defaultEventData,
...newValue,
participants: newValue?.participants ? [...newValue.participants] : []
};
}, { deep: true });
// 保存事件
const save = () => {
if (!eventData.value.title || !eventData.value.duration) {
// 标记字段为已触摸
touched.value.title = true;
touched.value.duration = true;
// 验证必填字段
if (!eventData.value.title.trim() || !eventData.value.duration) {
return;
}
emit('save', eventData.value);
// 过滤掉空白的参与人
const cleanedParticipants = eventData.value.participants?.filter(p => p.name.trim()) || [];
// 触发保存事件
emit('save', {
...eventData.value,
participants: cleanedParticipants
});
};
</script>
<style scoped>
/* 模态框动画 */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
/* 模态框容器动画 */
.modal-container {
transition: all 0.3s ease;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
will-change: transform, opacity;
}
</style>
/* 按钮通用样式 */
button {
transition: all 0.3s ease;
cursor: pointer;
}
/* 单选按钮样式 */
input[type="radio"] {
accent-color: #645eff;
}
/* 文本区域样式 */
textarea {
resize: none;
min-height: 80px;
}
/* 背景模糊效果 */
.backdrop-blur-sm {
backdrop-filter: blur(6px);
}
/* 通用过渡效果 */
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* 动画关键帧 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* 错误提示动画 */
.animate-pulse {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>

View File

@@ -1,75 +1,64 @@
<template>
<transition name="modal">
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-100 px-4 py-6 backdrop-blur-sm">
<transition
enter-active-class="transition-all duration-300 ease-out"
leave-active-class="transition-all duration-200 ease-in"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-if="showLoginModal" class="fixed inset-0 bg-gray-900 bg-opacity-70 flex items-center justify-center z-50 px-4 py-6 backdrop-blur-md">
<div class="modal-container bg-white rounded-2xl w-full max-w-md mx-auto shadow-soft-lg transition-all duration-300 transform scale-100">
<div class="p-8">
<div class="text-center mb-6">
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center mx-auto mb-4">
<i class="fas fa-calendar-alt text-white text-2xl"></i>
</div>
<h2 class="text-2xl font-bold text-gray-800">欢迎使用智能日程管家</h2>
<p class="text-gray-600 mt-2">请登录您的账号</p>
</div>
<div class="mb-5">
<label class="block text-sm font-medium text-gray-700 mb-1">用户名</label>
<input
v-model="loginData.username"
type="text"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
placeholder="liqi"
required
>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
<input
v-model="loginData.password"
type="password"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 outline-none transition-colors"
class="w-full px-4 py-3 rounded-lg border border-gray-200 focus:border-primary-500 focus:ring-2 focus:ring-primary-300 outline-none transition-colors duration-200"
placeholder="123456"
required
>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-1">团队</label>
<div class="px-4 py-3 bg-gray-50 rounded-lg border border-gray-200">
萧康云医技术开发团队-医生组
</div>
</div>
<button
@click="login"
class="w-full py-3 bg-gradient-to-r from-primary-500 to-primary-700 hover:from-primary-600 hover:to-primary-800 text-white rounded-xl font-medium transition-colors shadow-md hover:shadow-lg hover:-translate-y-0.5"
class="w-full py-3 bg-gradient-to-r from-primary-500 to-primary-700 hover:from-primary-600 hover:to-primary-800 text-white rounded-xl font-medium transition-all shadow-md hover:shadow-lg hover:-translate-y-0.5 active:translate-y-0"
>
登录
</button>
</div>
</div>
</div>
</transition>
</template>
<script setup>
import { ref } from 'vue';
@@ -85,34 +74,29 @@ const login = () => {
return;
}
// 模拟登录成功
// 登录成功
const user = {
id: 'user-001',
name: '李齐',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
avatar: 'https://api.dicebear.com/7.x/adventurer/svg?seed=Bella',
phone: '13800138000',
email: 'liqi@example.com',
team: '萧康云医技术开发团队-医生组'
};
// 保存到localStorage
localStorage.setItem('user', JSON.stringify(user));
emit('login', user);
};
</script>
<style scoped>
/* 模态框动画 */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-container {
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
button {
transition: all 0.3s ease;
cursor: pointer;
}
</style>

View File

@@ -144,3 +144,18 @@ input:focus, textarea:focus {
.error {
border-color: #ef4444;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background: linear-gradient(to bottom, #f0f4ff, #e0e7ff) no-repeat fixed;
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-in-out;
}

View File

@@ -1,5 +1,18 @@
import dayjs from 'dayjs';
import * as chineseLunar from 'chinese-lunar';
import { getLunar } from 'chinese-lunar-calendar'
const lunarMonthArray = [
'正', '二', '三', '四', '五', '六', '七', '八', '九', '十',
'十一', '十二'
];
const lunarArray = [
'初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十',
'十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十',
'廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十'
];
/**
* 获取农历日期
@@ -8,19 +21,16 @@ import * as chineseLunar from 'chinese-lunar';
*/
export function getLunarDate(date) {
try {
// 使用chinese-lunar库获取农历信息
const lunar = chineseLunar.solarToLunar(new Date(date));
const month = chineseLunar.format(lunar, 'M');
const day = chineseLunar.format(lunar, 'D');
const d = dayjs(date);
console.log(date, chineseLunar.dayName(lunar))
// console.log(date, lunar, month + day)
if (day === '初一') {
return month;
} else {
return day;
}
const year = d.year();
const month = d.month() + 1;
const day = d.date();
const result = getLunar(year, month, day);
console.log(getLunar(year, month, day));
return result.dateStr;
} catch (error) {
console.error('获取农历日期错误:', error);
return '';
@@ -116,4 +126,4 @@ export function isHoliday(date) {
console.error('判断假日错误:', error);
return false;
}
}
}