初始化
This commit is contained in:
@@ -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
45
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
483
src/App.vue
483
src/App.vue
@@ -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>
|
||||
403
src/components/CalendarView.vue
Normal file
403
src/components/CalendarView.vue
Normal 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>
|
||||
139
src/components/CommonEventModal.vue
Normal file
139
src/components/CommonEventModal.vue
Normal 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>
|
||||
195
src/components/CommonEvents.vue
Normal file
195
src/components/CommonEvents.vue
Normal 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>
|
||||
334
src/components/EventDetail.vue
Normal file
334
src/components/EventDetail.vue
Normal 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>
|
||||
215
src/components/EventModal.vue
Normal file
215
src/components/EventModal.vue
Normal 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>
|
||||
118
src/components/LoginModal.vue
Normal file
118
src/components/LoginModal.vue
Normal 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>
|
||||
@@ -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')
|
||||
|
||||
147
src/style.css
147
src/style.css
@@ -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
119
src/utils/calendarUtils.js
Normal 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
0
src/utils/utils.js
Normal file
43
tailwind.config.js
Normal file
43
tailwind.config.js
Normal 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: [],
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 8888,
|
||||
port: 17000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
// target: 'http://127.0.0.1:18007/api/',
|
||||
|
||||
Reference in New Issue
Block a user