初始化

This commit is contained in:
2025-06-10 17:03:02 +08:00
parent 67f9f51816
commit c0b62dfa0f
15 changed files with 2224 additions and 35 deletions

View File

@@ -9,18 +9,21 @@
"preview": "vite preview"
},
"dependencies": {
"tailwindcss": "^4.1.4",
"vue": "^3.5.13",
"@ant-design-vue/pro-layout": "^3.2.5",
"@ant-design/icons-vue": "^7.0.1",
"@fortawesome/fontawesome-free": "^6.7.2",
"@tailwindcss/vite": "^4.1.4",
"@vueuse/head": "^2.0.0",
"ant-design-vue": "^4.1.1",
"aos": "^2.3.4",
"axios": "^1.6.2",
"chinese-lunar": "^0.1.4",
"dayjs": "^1.11.13",
"lucide-vue-next": "^0.507.0",
"pinia": "^2.1.7",
"swiper": "^11.0.5",
"tailwindcss": "^4.1.4",
"vue": "^3.5.13",
"vue-router": "^4.2.5"
},
"devDependencies": {

45
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.13)
'@fortawesome/fontawesome-free':
specifier: ^6.7.2
version: 6.7.2
'@tailwindcss/vite':
specifier: ^4.1.4
version: 4.1.4(vite@6.3.3(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.88.0))
@@ -29,6 +32,12 @@ importers:
axios:
specifier: ^1.6.2
version: 1.9.0
chinese-lunar:
specifier: ^0.1.4
version: 0.1.4
dayjs:
specifier: ^1.11.13
version: 1.11.13
lucide-vue-next:
specifier: ^0.507.0
version: 0.507.0(vue@3.5.13)
@@ -263,6 +272,10 @@ packages:
cpu: [x64]
os: [win32]
'@fortawesome/fontawesome-free@6.7.2':
resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
engines: {node: '>=6'}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
@@ -295,42 +308,36 @@ 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==}
@@ -388,67 +395,56 @@ 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==}
@@ -506,28 +502,24 @@ 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==}
@@ -655,6 +647,9 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
chinese-lunar@0.1.4:
resolution: {integrity: sha512-E4seb9UZkSJtkm65Ry2ZLWTw5Z87DOyGcCePdg3ft7da342hnEliMtcDAPSn+YSo/o0xl1Egmhh671kxEJgY+g==}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -851,28 +846,24 @@ 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==}
@@ -1229,6 +1220,8 @@ snapshots:
'@esbuild/win32-x64@0.25.3':
optional: true
'@fortawesome/fontawesome-free@6.7.2': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@parcel/watcher-android-arm64@2.5.1':
@@ -1577,6 +1570,8 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
chinese-lunar@0.1.4: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2

View File

@@ -1,10 +1,481 @@
<script setup>
</script>
<template>
<router-view />
<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>
<style lang='scss' scoped>
<script setup>
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';
</style>
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);
}
});
</script>
<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;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
button:active {
transform: translateY(1px);
}
</style>

View File

@@ -0,0 +1,403 @@
<template>
<div class="bg-white rounded-2xl shadow-soft p-6">
<!-- 月视图 -->
<div v-if="view === 'month'" class="month-view">
<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 gap-2 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}"
class="calendar-cell dropzone rounded-xl border border-gray-100 p-3 cursor-pointer transition-all relative min-h-[120px]"
>
<!-- 日期和工作状态 -->
<div class="flex justify-between items-start">
<span
class="text-lg font-semibold w-8 h-8 flex items-center justify-center rounded-full"
: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 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 class="lunar-date text-xs text-gray-500 mt-1">
{{ getLunarDate(day.date) }}
</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>
</div>
</div>
</div>
<!-- 周视图 -->
<div v-else-if="view === 'week'" class="week-view">
<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"
>
<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
}"
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"
:style="{
backgroundColor: `${event.color}10`,
borderLeft: `3px solid ${event.color}`
}"
>
{{ event.title }}
</div>
</div>
</div>
</div>
</div>
<!-- 日视图 -->
<div v-else class="day-view">
<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="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"
: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 } 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 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 getFirstEventTitle = (date) => {
const events = props.events.filter(e => e.date === date);
return events.length > 0 ? events[0].title : '';
};
// 获取当天所有事件颜色
const getEventColorsForDay = (date) => {
const events = props.events.filter(e => e.date === date);
return [...new Set(events.map(e => e.color))];
};
// 检查是否是今天
const isToday = (date) => {
return dayjs(date).isSame(dayjs(), 'day');
};
// 是否为调休工作日
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);
};
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) => {
event.currentTarget.classList.add('drag-over');
};
// 处理拖拽离开
const handleDragLeave = (event) => {
event.currentTarget.classList.remove('drag-over');
};
// 处理放置事件
const handleDropEvent = (event, date, time = null) => {
event.currentTarget.classList.remove('drag-over');
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.1);
box-shadow: inset 0 0 0 2px #645eff;
}
.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>

View File

@@ -0,0 +1,139 @@
<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"
>
添加快捷日程
</button>
</div>
</div>
</div>
</div>
</transition>
</template>
<script setup>
import { ref } from 'vue';
const emit = defineEmits(['close', 'save']);
const colors = [
'#645eff',
'#0fb8a9',
'#f0af56',
'#e85d75',
'#8c6bed'
];
const eventData = ref({
title: '',
duration: 30,
color: colors[1]
});
const save = () => {
if (!eventData.value.title.trim()) {
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;
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div>
<!-- 说明卡片 -->
<div class="bg-white rounded-2xl shadow-soft overflow-hidden mb-6" data-aos="zoom-in">
<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">
<ul class="space-y-4">
<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">
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="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"
>
<i class="fas fa-plus text-white text-sm"></i>
</button>
</div>
<div class="p-5 max-h-[420px] overflow-y-auto">
<div class="space-y-3">
<div
v-for="(event, index) in commonEvents"
:key="'common-'+index"
draggable="true"
@dragstart="handleDragStart($event, event)"
@dragend="handleDragEnd"
class="common-event group flex items-center px-4 py-3 rounded-xl border border-gray-100 bg-white cursor-move transition-all relative hover:shadow-md"
>
<div
class="absolute top-0 left-0 w-1 h-full rounded-l"
:style="{ backgroundColor: event.color }"
></div>
<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"
>
<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"
>
<i class="fas fa-plus-circle mr-2"></i>
新增快捷日程
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
commonEvents: Array
});
const emit = defineEmits(['add-common-event', 'drag-start', 'drag-end']);
const handleDragStart = (event, commonEvent) => {
emit('drag-start', event, commonEvent, 'common');
};
const handleDragEnd = () => {
emit('drag-end');
};
</script>
<style scoped>
.common-event {
transition: all 0.3s ease;
cursor: grab;
}
.common-event:hover {
transform: translateX(5px);
}
</style>

View File

@@ -0,0 +1,334 @@
<template>
<div
class="bg-white rounded-2xl shadow-soft p-6 h-full"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
:class="{'drag-over': isDragOver}"
>
<div
v-if="!selectedDate"
class="h-full flex flex-col items-center justify-center text-center p-6"
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>
</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">
<h2 class="text-xl font-bold text-gray-800">
{{ dayjs(selectedDate).format('MM月DD日') }}
<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"
>
<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) }}
</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) }}
</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>
休息日
</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>
工作日
</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">
<i class="fas fa-tasks text-primary-600 mr-2"></i>
今日日程
</h3>
<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)"
:key="event.id"
draggable="true"
@dragstart="handleDragStart($event, event)"
@dragend="handleDragEnd"
class="schedule-item p-4 rounded-xl cursor-pointer border-l-4 hover:shadow-md"
:style="{
backgroundColor: `${event.color}08`,
borderLeftColor: event.color
}"
data-aos="fade-up"
:data-aos-delay="50 * index"
>
<div class="flex items-start">
<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"
>
<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
v-if="event.location"
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 || '测试' }}
</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">
<i class="fas fa-user mr-1 text-sm text-gray-500"></i>
<span class="text-sm">{{ participant.name }}</span>
</div>
</div>
</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"
>
<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';
import { getLunarDate, getFestival, getSolarTerm, isHoliday } from '@/utils/calendarUtils';
const props = defineProps({
selectedDate: String,
events: Array,
commonEvents: Array
});
const emit = defineEmits(['add-event', 'edit-event', 'drop-event', 'drag-start', 'drag-end']);
const isDragOver = ref(false);
// 获取当天的日程
const getEventsForDate = (date) => {
return props.events.filter(e => e.date === date);
};
// 处理拖拽开始
const handleDragStart = (event, eventItem) => {
emit('drag-start', event, eventItem, 'event');
};
// 处理拖拽结束
const handleDragEnd = () => {
emit('drag-end');
};
// 处理拖拽悬停
const handleDragOver = (event) => {
isDragOver.value = true;
event.preventDefault();
};
// 处理拖拽离开
const handleDragLeave = (event) => {
isDragOver.value = false;
};
// 处理放置事件
const handleDrop = (event) => {
isDragOver.value = false;
emit('drop-event', event, props.selectedDate);
};
</script>
<style scoped>
.event-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
}
.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);
}
.drag-over {
background-color: rgba(100, 94, 255, 0.1);
box-shadow: inset 0 0 0 2px #645eff;
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);
}
}
</style>

View File

@@ -0,0 +1,215 @@
<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>
</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.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"
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"
>
</div>
<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="480"
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>
<!-- 新增字段 -->
<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>
</div>
<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"
:style="{ backgroundColor: color }"
></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>
</div>
</transition>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
event: Object,
isEditing: Boolean
});
const emit = defineEmits(['close', 'save', 'delete']);
const colors = [
'#645eff',
'#0fb8a9',
'#f0af56',
'#e85d75',
'#8c6bed'
];
const eventData = ref({ ...props.event });
// 当传入的 event prop 变化时更新本地值
watch(() => props.event, (newValue) => {
eventData.value = { ...newValue };
});
const save = () => {
if (!eventData.value.title || !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;
}
</style>

View File

@@ -0,0 +1,118 @@
<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">
<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"
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"
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"
>
登录
</button>
</div>
</div>
</div>
</transition>
</template>
<script setup>
import { ref } from 'vue';
const emit = defineEmits(['login', 'close']);
const loginData = ref({
username: 'liqi',
password: '123456'
});
const login = () => {
if (!loginData.value.username || !loginData.value.password) {
return;
}
// 模拟登录成功
const user = {
id: 'user-001',
name: '李齐',
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
phone: '13800138000',
email: 'liqi@example.com',
team: '萧康云医技术开发团队-医生组'
};
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 ease;
}
</style>

View File

@@ -5,6 +5,9 @@ import './style.css'
import App from './App.vue'
// 注册路由
import router from './router'
import '@fortawesome/fontawesome-free/css/all.min.css';
import AOS from 'aos'
import 'aos/dist/aos.css'
const app = createApp(App)
@@ -13,4 +16,10 @@ const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(Antd)
app.use(AOS.init({
duration: 800,
offset: 100,
easing: 'ease-in-out',
once: true
}))
app.mount('#app')

View File

@@ -1 +1,146 @@
@import "tailwindcss";
@import "tailwindcss";
:root {
--primary-50: #f5f7ff;
--primary-100: #eef1ff;
--primary-200: #dee4ff;
--primary-300: #c3ceff;
--primary-400: #a1abff;
--primary-500: #7f88ff;
--primary-600: #645eff;
--primary-700: #4f47e9;
--primary-800: #423cc4;
--primary-900: #39369e;
--secondary-50: #f0fdfa;
--secondary-100: #cffaf0;
--secondary-200: #9af4e3;
--secondary-300: #5de8d4;
--secondary-400: #2bd4c0;
--secondary-500: #0fb8a9;
--secondary-600: #07938c;
--secondary-700: #0b746f;
--secondary-800: #0f5c59;
--secondary-900: #114c4a;
--accent-500: #ff7b54;
}
.bg-primary-50 {
background-color: var(--primary-50);
}
.bg-primary-100 {
background-color: var(--primary-100);
}
.bg-primary-500 {
background-color: var(--primary-500);
}
.bg-primary-600 {
background-color: var(--primary-600);
}
.bg-primary-700 {
background-color: var(--primary-700);
}
.bg-gradient-to-r {
background-image: linear-gradient(to right, var(--tw-gradient-stops));
}
.from-primary-500 {
--tw-gradient-from: var(--primary-500);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-primary-700 {
--tw-gradient-to: var(--primary-700);
}
.from-secondary-500 {
--tw-gradient-from: var(--secondary-500);
--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}
.to-secondary-700 {
--tw-gradient-to: var(--secondary-700);
}
.text-primary-500 {
color: var(--primary-500);
}
.text-primary-600 {
color: var(--primary-600);
}
.bg-secondary-500 {
background-color: var(--secondary-500);
}
.text-white {
color: #fff;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-700 {
color: #374151;
}
.text-gray-600 {
color: #4b5563;
}
.text-gray-500 {
color: #6b7280;
}
.text-gray-400 {
color: #9ca3af;
}
.bg-white {
background-color: #fff;
}
.bg-gray-50 {
background-color: #f9fafb;
}
.bg-gray-100 {
background-color: #f3f4f6;
}
.bg-red-50 {
background-color: #fef2f2;
}
.text-red-500 {
color: #ef4444;
}
.bg-green-50 {
background-color: #f0fdf4;
}
.text-green-600 {
color: #16a34a;
}
.text-amber-600 {
color: #d97706;
}
.shadow-soft {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
}
.shadow-soft-lg {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
}
/* 按钮悬停效果 */
button, .btn {
transition: all 0.3s ease;
cursor: pointer;
}
button:hover, .btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
button:active, .btn:active {
transform: translateY(1px);
}
/* 表单焦点样式 */
input:focus, textarea:focus {
border-color: var(--primary-500);
box-shadow: 0 0 0 3px rgba(100, 94, 255, 0.1);
}
/* 错误状态 */
.error {
border-color: #ef4444;
}

119
src/utils/calendarUtils.js Normal file
View File

@@ -0,0 +1,119 @@
import dayjs from 'dayjs';
import * as chineseLunar from 'chinese-lunar';
/**
* 获取农历日期
* @param {string} date - YYYY-MM-DD格式的日期
* @returns {string} 农历日期(初一显示月份)
*/
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');
console.log(date, chineseLunar.dayName(lunar))
// console.log(date, lunar, month + day)
if (day === '初一') {
return month;
} else {
return day;
}
} catch (error) {
console.error('获取农历日期错误:', error);
return '';
}
}
/**
* 获取节假日信息
* @param {string} date - YYYY-MM-DD格式的日期
* @returns {string|null} 节假日名称如果没有则为null
*/
export function getFestival(date) {
try {
return '';
// 使用dayjs解析日期
const d = dayjs(date);
const month = d.month() + 1;
const day = d.date();
// 公历节日
if (month === 1 && day === 1) return '元旦';
if (month === 5 && day === 1) return '劳动节';
if (month === 10 && day === 1) return '国庆节';
// 农历节日 - 使用chinese-lunar获取农历日期
const year = d.year();
const lunar = chineseLunar.solarToLunar(new Date(date));
// 农历节日
if (lunar.lunarMonth === 1 && lunar.lunarDay === 1) return '春节';
if (lunar.lunarMonth === 1 && lunar.lunarDay === 15) return '元宵节';
if (lunar.lunarMonth === 5 && lunar.lunarDay === 5) return '端午节';
if (lunar.lunarMonth === 8 && lunar.lunarDay === 15) return '中秋节';
if (lunar.lunarMonth === 9 && lunar.lunarDay === 9) return '重阳节';
return null;
} catch (error) {
console.error('获取节日错误:', error);
return null;
}
}
/**
* 获取节气信息
* @param {string} date - YYYY-MM-DD格式的日期
* @returns {string|null} 节气名称如果没有则为null
*/
export function getSolarTerm(date) {
return '大雪';
const d = dayjs(date);
const month = d.month() + 1;
const day = d.date();
// 24节气简化版
const terms = [
// 每个月的两个节气
[1, 5, '小寒'], [1, 20, '大寒'],
[2, 4, '立春'], [2, 19, '雨水'],
[3, 5, '惊蛰'], [3, 20, '春分'],
[4, 5, '清明'], [4, 20, '谷雨'],
[5, 5, '立夏'], [5, 21, '小满'],
[6, 6, '芒种'], [6, 21, '夏至'],
[7, 7, '小暑'], [7, 23, '大暑'],
[8, 7, '立秋'], [8, 23, '处暑'],
[9, 8, '白露'], [9, 23, '秋分'],
[10, 8, '寒露'], [10, 23, '霜降'],
[11, 7, '立冬'], [11, 22, '小雪'],
[12, 7, '大雪'], [12, 22, '冬至']
];
}
/**
* 是否为法定假日
* @param {string} date - YYYY-MM-DD格式的日期
* @returns {boolean} 是否为法定假日
*/
export function isHoliday(date) {
try {
// 使用dayjs解析日期
const d = dayjs(date);
const dayOfWeek = d.day();
// 周六或周日为休息日
if (dayOfWeek === 0 || dayOfWeek === 6) {
return true;
}
// 检查是否为法定节假日
const festival = getFestival(date);
return !!festival;
} catch (error) {
console.error('判断假日错误:', error);
return false;
}
}

0
src/utils/utils.js Normal file
View File

43
tailwind.config.js Normal file
View File

@@ -0,0 +1,43 @@
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: {
50: '#f5f7ff',
100: '#eef1ff',
200: '#dee4ff',
300: '#c3ceff',
400: '#a1abff',
500: '#7f88ff',
600: '#645eff',
700: '#4f47e9',
800: '#423cc4',
900: '#39369e',
950: '#201f59',
},
secondary: {
50: '#f0fdfa',
100: '#cffaf0',
200: '#9af4e3',
300: '#5de8d4',
400: '#2bd4c0',
500: '#0fb8a9',
600: '#07938c',
700: '#0b746f',
800: '#0f5c59',
900: '#114c4a',
950: '#022c2c',
}
},
boxShadow: {
'soft': '0 8px 32px rgba(31, 38, 135, 0.1)',
'soft-lg': '0 12px 48px rgba(31, 38, 135, 0.15)',
}
}
},
plugins: [],
}

View File

@@ -15,7 +15,7 @@ export default defineConfig({
}
},
server: {
port: 8888,
port: 17000,
proxy: {
'/api': {
// target: 'http://127.0.0.1:18007/api/',