feat: 新增今日活動行事曆

This commit is contained in:
MJM_2025_05\polly 2025-09-05 17:28:43 +08:00
parent d785ab54bd
commit d5bc26e5a6
9 changed files with 466 additions and 165 deletions

74
package-lock.json generated
View File

@ -8,6 +8,12 @@
"name": "uark_front",
"version": "0.0.0",
"dependencies": {
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/list": "^6.1.19",
"@fullcalendar/timegrid": "^6.1.19",
"@fullcalendar/vue3": "^6.1.19",
"axios": "^1.11.0",
"echarts": "^6.0.0",
"leaflet": "^1.9.4",
@ -534,6 +540,64 @@
"node": ">=18"
}
},
"node_modules/@fullcalendar/core": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.19.tgz",
"integrity": "sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ==",
"license": "MIT",
"dependencies": {
"preact": "~10.12.1"
}
},
"node_modules/@fullcalendar/daygrid": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.19.tgz",
"integrity": "sha512-IAAfnMICnVWPjpT4zi87i3FEw0xxSza0avqY/HedKEz+l5MTBYvCDPOWDATpzXoLut3aACsjktIyw9thvIcRYQ==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@fullcalendar/interaction": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.19.tgz",
"integrity": "sha512-GOciy79xe8JMVp+1evAU3ytdwN/7tv35t5i1vFkifiuWcQMLC/JnLg/RA2s4sYmQwoYhTw/p4GLcP0gO5B3X5w==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@fullcalendar/list": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.19.tgz",
"integrity": "sha512-knZHpAVF0LbzZpSJSUmLUUzF0XlU/MRGK+Py2s0/mP93bCtno1k2L3XPs/kzh528hSjehwLm89RgKTSfW1P6cA==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@fullcalendar/timegrid": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.19.tgz",
"integrity": "sha512-OuzpUueyO9wB5OZ8rs7TWIoqvu4v3yEqdDxZ2VcsMldCpYJRiOe7yHWKr4ap5Tb0fs7Rjbserc/b6Nt7ol6BRg==",
"license": "MIT",
"dependencies": {
"@fullcalendar/daygrid": "~6.1.19"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.19"
}
},
"node_modules/@fullcalendar/vue3": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/vue3/-/vue3-6.1.19.tgz",
"integrity": "sha512-j5eUSxx0xIy3ADljo0f5B9PhjqXnCQ+7nUMPfsslc2eGVjp4F74YvY3dyd6OBbg13IvpsjowkjncGipYMQWmTA==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.19",
"vue": "^3.0.11"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -2561,6 +2625,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",

View File

@ -9,6 +9,12 @@
"preview": "vite preview"
},
"dependencies": {
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/list": "^6.1.19",
"@fullcalendar/timegrid": "^6.1.19",
"@fullcalendar/vue3": "^6.1.19",
"axios": "^1.11.0",
"echarts": "^6.0.0",
"leaflet": "^1.9.4",

View File

@ -1,8 +1,8 @@
<template>
<section id="app" class="flex flex-col min-h-screen ">
<section id="app" class="flex flex-col min-h-screen">
<NavBar class="fixed" />
<main class="w-full p-4 overflow-x-hidden pb-4">
<RouterView />
<main class="w-full p-4 overflow-x-hidden">
<RouterView class="h-[calc(100vh-72px-40px)]" />
</main>
</section>
</template>

View File

@ -14,7 +14,7 @@
<!-- 右側路由內容 -->
<section class="col-span-4 h-full overflow-hidden">
<div class="w-full h-full">
<RouterView />
<RouterView class="h-[calc(100vh-72px-24px)]" />
</div>
</section>
</section>

View File

@ -530,19 +530,13 @@ const triggerRef = ref(null);
const panelRef = ref(null);
const facilities = [
"崇恩護理之家",
"崇祐護理之家",
"護祐護理之家",
"育祐護理之家",
"崇智護理之家",
"崇恩居家護理所",
"傳心日間照顧中心",
"敬慈居家服務中心",
"崇恩長照中心(養護型)",
"崇祐長照中心(養護型)",
"傳祐長照中心(養護型)",
"慈祐長照中心(養護型)",
"中安崇恩長照中心(養護型)",
"A 機構",
"B 機構",
"C 機構",
"D 機構",
"E 機構",
"F 機構",
];
const selectedItem = ref(facilities[0]);

View File

@ -1,6 +1,6 @@
<template>
<section
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 lg:h-[calc(100vh-72px-32px)] h-auto justify-center overflow-anchor:none"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 justify-center"
>
<!-- 左側照片/說明 + 進度條 + 圖表 + 葷素資料 -->
<section
@ -70,16 +70,16 @@
</div>
<!-- 照片 + 說明手機隱藏平板/桌機顯示 -->
<div
class="w-full grid grid-rows-12 justify-start items-start gap-6 hidden sm:grid"
class="w-full grid grid-rows-12 justify-start items-start gap-4 hidden sm:grid"
>
<div class="h-[200px] row-span-6">
<div class="h-[230px] row-span-7">
<img
:src="currentFacility.photo"
alt="機構照片"
class="w-full h-full rounded-sm object-cover"
/>
</div>
<div class="row-span-6">
<div class="row-span-5">
<p
class="text-brand-gray bg-white/70 text-sm border rounded-sm px-2 py-3 border-brand-gray-light"
>
@ -90,8 +90,7 @@
</div>
<!-- Progress bars -->
<!-- Progress bars加上 key 以觸發重新掛載同時傳遞動畫相關 attrs 給子元件 <progress> -->
<div class="w-full flex flex-col gap-4">
<div class="w-full flex flex-col gap-5">
<ProgressBar
:key="`residents-${selectedFacility}`"
label="現在住民/全立案床數"
@ -183,9 +182,9 @@
<!-- 素資料 -->
<section class="row-span-3 grid grid-cols-2 gap-2">
<!-- -->
<div class="col-span-1 bg-white/70 rounded-md shadow p-6">
<div class="col-span-1 bg-white/70 rounded-md shadow p-4">
<div
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-start items-center gap-3 p-3"
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-center items-center gap-4"
>
<div class="relative text-brand-yellow">
<svg
@ -205,7 +204,7 @@
</p>
</div>
<div class="flex gap-6">
<div class="flex gap-4">
<div class="flex flex-col justify-center items-start gap-3">
<p>總數{{ currentFacility.diet.meat.total }}</p>
<p>一般{{ currentFacility.diet.meat.normal }}</p>
@ -218,9 +217,9 @@
</div>
</div>
<!-- -->
<div class="col-span-1 bg-white/70 rounded-md shadow p-6">
<div class="col-span-1 bg-white/70 rounded-md shadow p-4">
<div
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-start items-center gap-3 p-3"
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-center items-center gap-4"
>
<div class="relative text-brand-yellow">
<svg
@ -240,7 +239,7 @@
</p>
</div>
<div class="flex gap-6">
<div class="flex gap-4">
<div class="flex flex-col justify-center items-start gap-3">
<p>總數{{ currentFacility.diet.veg.total }}</p>
<p>一般{{ currentFacility.diet.veg.normal }}</p>
@ -349,7 +348,7 @@
</div>
</div>
<!-- 地圖本體刻意壓低 z-index -->
<!-- 地圖本體 -->
<div
ref="mapEl"
class="relative z-[0] w-full flex-1 rounded-md overflow-hidden min-h-[300px] md:min-h-[360px] lg:min-h-[420px]"
@ -360,18 +359,30 @@
<section class="flex flex-col gap-2 order-4 lg:order-3">
<!-- 今日活動 -->
<section
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3"
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3 h-[360px]"
>
<!-- 標題列左右分佈 -->
<div class="flex items-center justify-between">
<h3 class="text-2xl font-bold">今日活動</h3>
<div class="flex flex-col gap-4 mb-6">
<div class="w-full overflow-x-auto overflow-y-auto">
<button
class="btn btn-sm bg-brand-green-light text-brand-black hover:bg-brand-purple-light border-none shadow rounded px-3 py-2 tracking-wider"
type="button"
@click="openCalendar()"
aria-haspopup="dialog"
aria-controls="calendar-modal"
>
行事曆
</button>
</div>
<!-- 表格區 -->
<div class="flex-1 overflow-y-auto">
<table class="table whitespace-nowrap">
<thead
class="bg-brand-gray-lighter text-brand-black sticky top-0 z-[1]"
>
<tr>
<th class="w-[120px]">時間</th>
<th class="w-[160px]">機構</th>
<th class="w-[160px]">機構名稱</th>
<th class="w-[160px]">活動名稱</th>
</tr>
</thead>
@ -388,7 +399,7 @@
</tbody>
</table>
</div>
</div>
<div class="mt-3 flex items-center justify-between px-3">
<span class="text-sm text-brand-gray"
> {{ total }} 每頁 {{ pageSize }} </span
@ -415,20 +426,64 @@
</div>
</section>
<!-- FullCalendar Modal -->
<transition name="fade">
<div
v-if="isCalendarOpen"
id="calendar-modal"
role="dialog"
aria-modal="true"
class="fixed inset-0 z-[2000] flex items-center justify-center"
@keydown.esc="closeCalendar()"
>
<!-- 背景遮罩 -->
<div
class="absolute inset-0 bg-black/40"
@click="closeCalendar()"
></div>
<!-- 置中面板 -->
<div
class="relative bg-white rounded-lg shadow-xl w-[48vw] max-w-6xl h-[84vh] p-6 flex flex-col gap-6"
>
<!-- FullCalendar 容器 -->
<div class="flex justify-start items-center">
<h4 class="text-2xl font-semibold">今日活動行事曆</h4>
</div>
<div class="flex-1 min-h-0">
<div
:style="[fcThemeVars, fcButtonVars, fcSizeVars]"
class="h-full w-full"
>
<FullCalendar :options="calendarOptions" />
</div>
</div>
<div class="flex items-center justify-end">
<button
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple"
type="button"
@click="closeCalendar()"
>
關閉
</button>
</div>
</div>
</div>
</transition>
<!-- 今日異常事件 -->
<section
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3"
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3 h-[360px]"
>
<h3 class="text-2xl font-bold">今日異常事件</h3>
<div class="flex flex-col gap-4 mb-6">
<div class="w-full overflow-x-auto overflow-y-auto">
<table class="table whitespace-nowrap">
<div class="flex-1 overflow-y-auto">
<table class="table whitespace-nowrap min-w-full">
<thead
class="bg-brand-gray-lighter text-brand-black sticky top-0 z-[1]"
>
<tr>
<th class="w-[100px]">時間</th>
<th class="w-[160px]">機構</th>
<th class="w-[160px]">機構名稱</th>
<th class="w-[100px]">事件</th>
</tr>
</thead>
@ -436,16 +491,16 @@
<tr
v-for="row in pagedIncidentRows"
:key="row.id"
class="transition-colors duration-150 hover:bg-brand-gray-lighter focus-visible:bg-gray-100/70"
class="hover:bg-brand-gray-lighter"
>
<td>{{ row.time }}</td>
<td class="truncate" :title="row.org">{{ row.org }}</td>
<td class="truncate" :title="row.event">{{ row.event }}</td>
<td class="truncate">{{ row.org }}</td>
<td class="truncate">{{ row.event }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mt-3 flex items-center justify-between">
<span class="ml-3 text-sm text-brand-gray"
> {{ incidentTotal }} 每頁 {{ incidentPageSize }} </span
@ -474,18 +529,17 @@
<!-- 今日派車總表 -->
<section
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3"
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3 h-[360px]"
>
<h3 class="text-2xl font-bold">今日派車總表</h3>
<div class="flex flex-col gap-4 mb-6">
<div class="w-full overflow-x-auto">
<table class="table whitespace-nowrap">
<div class="flex-1 overflow-y-auto">
<table class="table whitespace-nowrap min-w-full">
<thead
class="bg-brand-gray-lighter text-brand-black sticky top-0 z-[1]"
>
<tr>
<th class="w-[120px]">時間</th>
<th class="w-[160px]">機構</th>
<th class="w-[160px]">機構名稱</th>
<th class="w-[120px]">聯絡人</th>
</tr>
</thead>
@ -493,18 +547,15 @@
<tr
v-for="row in pagedDispatchRows"
:key="row.id"
class="transition-colors duration-150 hover:bg-brand-gray-lighter focus-visible:bg-gray-100/70"
class="hover:bg-brand-gray-lighter"
>
<td>{{ row.time }}</td>
<td class="truncate" :title="row.org">{{ row.org }}</td>
<td class="truncate" :title="row.contact">
{{ row.contact }}
</td>
<td class="truncate">{{ row.org }}</td>
<td class="truncate">{{ row.contact }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mt-3 flex items-center justify-between">
<span class="ml-3 text-sm text-brand-gray"
> {{ dispatchTotal }} 每頁 {{ dispatchPageSize }} </span
@ -536,6 +587,12 @@
<script setup>
// ===== Imports =====
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import listPlugin from "@fullcalendar/list";
import zhTw from "@fullcalendar/core/locales/zh-tw";
import ProgressBar from "./ProgressBar.vue";
import {
ref,
@ -558,37 +615,37 @@ const KAOHSIUNG_BOUNDS_LOOSE = L.latLngBounds([22.35, 120.0], [23.05, 120.75]);
// fetch
const LOCATIONS = [
{
name: "崇恩護理之家",
name: "A 機構",
addr: "高雄市楠梓區立仁街131、133號",
lat: 22.7409648895,
lng: 120.3354644775,
},
{
name: "育祐護理之家",
name: "B 機構",
addr: "高雄市楠梓區常德路317巷9弄27號",
lat: 22.7366924286,
lng: 120.3363342285,
},
{
name: "崇祐護理之家",
name: "C 機構",
addr: "高雄市楠梓區宏昌街135、137號",
lat: 22.717588,
lng: 120.29406,
},
{
name: "崇智護理之家",
name: "D 機構",
addr: "高雄市左營區民族一路980號",
lat: 22.678054,
lng: 120.3192,
},
{
name: "護祐護理之家",
name: "E 機構",
addr: "高雄市三民區黃興路336號",
lat: 22.651488,
lng: 120.33731,
},
{
name: "傳祐長照中心",
name: "F 機構",
addr: "高雄市小港區沿海一路377號",
lat: 22.5644512177,
lng: 120.3544540405,
@ -601,13 +658,13 @@ const iconBase = `${base}img/leaflet/`;
// building_1 = 2~7 =
const FACILITY_ASSETS = {
ALL: `${base}img/building/building_1.jpg`, // `${base}img/building/headquarter.png`
崇恩護理之家: `${base}img/building/building_2.jpg`,
育祐護理之家: `${base}img/building/building_3.jpg`,
崇祐護理之家: `${base}img/building/building_4.jpg`,
崇智護理之家: `${base}img/building/building_5.jpg`,
護祐護理之家: `${base}img/building/building_6.jpg`,
傳祐長照中心: `${base}img/building/building_7.jpg`,
ALL: `${base}img/building/building_1.jpg`, // `${base}img/building/headquarter.png`
"A 機構": `${base}img/building/building_2.jpg`,
"B 機構": `${base}img/building/building_3.jpg`,
"C 機構": `${base}img/building/building_4.jpg`,
"D 機構": `${base}img/building/building_5.jpg`,
"E 機構": `${base}img/building/building_6.jpg`,
"F 機構": `${base}img/building/building_7.jpg`,
};
// Leaflet
@ -625,8 +682,7 @@ L.Marker.prototype.options.icon = defaultIcon;
// ===== Facility Mock Data () =====
const FACILITY_DATA = {
ALL: {
photo: "/img/building/headquarter.png",
desc: "崇恩長期照顧集團——大南部在地深耕,整合醫護團隊與專業照護人員,提供高齡照護服務總部。",
desc: "OO長期照顧集團——在全台各地皆有眾多據點,整合醫護團隊與專業照護人員,提供高齡照護服務總部。",
progress: {
residents: { current: 240, total: 360 },
vacancy: { current: 120, total: 360 },
@ -666,9 +722,8 @@ const FACILITY_DATA = {
veg: { total: 367, normal: 318, soft: 26, tube: 23 },
},
},
崇恩護理之家: {
photo: "/img/building/chong-en.png",
desc: "崇恩護理之家重視跨專業合作,提供 24 小時專業護理與生活照顧服務。",
"A 機構": {
desc: "A 機構重視跨專業合作,提供 24 小時專業護理與生活照顧服務Lorem ipsum dolor sit amet consectetur adipisicing elit。",
progress: {
residents: { current: 36, total: 49 },
vacancy: { current: 12, total: 49 },
@ -708,9 +763,8 @@ const FACILITY_DATA = {
veg: { total: 82, normal: 72, soft: 5, tube: 5 },
},
},
育祐護理之家: {
photo: "/img/building/yu-you.png",
desc: "育祐護理之家以長者需求為中心,打造安全舒適的照護環境。",
"B 機構": {
desc: "B 機構以長者需求為中心打造安全舒適的照護環境Lorem ipsum dolor sit amet, consectetur adipisicing elit.。",
progress: {
residents: { current: 42, total: 60 },
vacancy: { current: 8, total: 60 },
@ -750,9 +804,8 @@ const FACILITY_DATA = {
veg: { total: 61, normal: 53, soft: 5, tube: 3 },
},
},
崇祐護理之家: {
photo: "/img/building/chong-you.png",
desc: "崇祐護理之家強調復健與社會參與,陪伴長者建立日常節奏。",
"C 機構": {
desc: "C 機構強調復健與社會參與陪伴長者建立日常節奏Lorem ipsum dolor sit amet consectetur adipisicing elit。",
progress: {
residents: { current: 50, total: 70 },
vacancy: { current: 15, total: 70 },
@ -792,9 +845,8 @@ const FACILITY_DATA = {
veg: { total: 71, normal: 62, soft: 5, tube: 4 },
},
},
崇智護理之家: {
photo: "/img/building/chong-zhi.png",
desc: "崇智護理之家導入智慧照護設備,提升照護品質與效率。",
"D 機構": {
desc: "D 機構導入智慧照護設備提升照護品質與效率Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro, enim。",
progress: {
residents: { current: 38, total: 55 },
vacancy: { current: 10, total: 55 },
@ -834,9 +886,8 @@ const FACILITY_DATA = {
veg: { total: 51, normal: 44, soft: 3, tube: 5 },
},
},
護祐護理之家: {
photo: "/img/building/hu-you.png",
desc: "護祐護理之家著重個別化照護計畫,維護長者尊嚴與生活品質。",
"E 機構": {
desc: "E 機構著重個別化照護計畫維護長者尊嚴與生活品質Lorem ipsum dolor sit amet, consectetur adipisicing elit.。",
progress: {
residents: { current: 45, total: 62 },
vacancy: { current: 9, total: 62 },
@ -876,9 +927,8 @@ const FACILITY_DATA = {
veg: { total: 57, normal: 49, soft: 5, tube: 3 },
},
},
傳祐長照中心: {
photo: "/img/building/chuan-you.png",
desc: "傳祐長照中心以社區融入為核心,串連日照與居家照護資源。",
"F 機構": {
desc: "F 機構以社區融入為核心串連日照與居家照護資源Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro, enim。",
progress: {
residents: { current: 30, total: 40 },
vacancy: { current: 6, total: 40 },
@ -1209,10 +1259,10 @@ function syncTooltips() {
}
}
}
if (map?.invalidateSize) {
const m = map;
if (m?.invalidateSize) {
requestAnimationFrame(() => {
map.invalidateSize();
m?.invalidateSize?.();
refreshAllTooltips();
});
}
@ -1269,9 +1319,10 @@ onMounted(() => {
// invalidateSize
requestAnimationFrame(() => {
map?.invalidateSize?.();
const m = map; // map
m?.invalidateSize?.();
requestAnimationFrame(() => {
map?.invalidateSize?.();
m?.invalidateSize?.();
refreshAllTooltips();
});
});
@ -1328,10 +1379,20 @@ watch(selectedFacility, () => {
});
// ===== Right Pane Tables () =====
// ====== ======
const today = new Date();
const yyyy = today.getFullYear();
const mm = String(today.getMonth() + 1).padStart(2, "0");
const dd = String(today.getDate()).padStart(2, "0");
const todayISO = `${yyyy}-${mm}-${dd}`;
const rows = ref([
{ id: 1, time: "10:00", org: "崇祐護理之家", title: "全人評估相關課程" },
{ id: 2, time: "14:00", org: "崇祐護理之家", title: "CPR教育訓練" },
{ id: 3, time: "20:00", org: "傳祐長照中心", title: "消防夜間演練" },
{ id: 1, time: "09:15", org: "A 機構", title: "全人評估相關課程" },
{ id: 2, time: "10:00", org: "B 機構", title: "CPR 教育訓練" },
{ id: 3, time: "10:30", org: "C 機構", title: "消防夜間演練說明會" },
{ id: 4, time: "13:00", org: "D 機構", title: "長照照護法規研習" },
{ id: 5, time: "14:30", org: "E 機構", title: "復健團體活動" },
{ id: 6, time: "15:45", org: "F 機構", title: "營養衛教與點心時間" },
]);
const pageSize = 10;
@ -1346,10 +1407,93 @@ const pagedRows = computed(() =>
(currentPage.value - 1) * pageSize + pageSize
)
);
// ===== FullCalendar Theme Vars brand=====
const FC_FONT =
'"Noto Sans TC","Noto Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"PingFang TC","Microsoft JhengHei",sans-serif';
// style .fc
const fcThemeVars = computed(() => ({
//
"--fc-page-font-family": FC_FONT,
//
"--fc-event-bg-color": brand.purpleLight,
"--fc-event-border-color": brand.purple,
"--fc-event-text-color": brand.black,
// Buttons////
"--fc-button-text-color": brand.white,
"--fc-button-bg-color": brand.purpleDark, //
"--fc-button-border-color": brand.purpleDark,
"--fc-button-hover-bg-color": brand.purple, //
"--fc-button-hover-border-color": brand.purple,
"--fc-button-active-bg-color": brand.purple, // current view
"--fc-button-active-border-color": brand.purple,
// #RRGGBBAA 66 40%
"--fc-today-bg-color": `${brand.yellow}30`,
// //hover
"--fc-neutral-bg-color": brand.grayLighter,
"--fc-neutral-text-color": brand.gray,
"--fc-border-color": brand.grayLight,
"--fc-list-event-hover-bg-color": brand.grayLighter,
//
"--fc-now-indicator-color": brand.red,
}));
// ===== Calendar state / methods =====
const isCalendarOpen = ref(false);
const openCalendar = () => (isCalendarOpen.value = true);
const closeCalendar = () => (isCalendarOpen.value = false);
// todayISO / rows
const calendarEvents = computed(() =>
rows.value.map((r) => ({
id: String(r.id),
title: `[${r.org}] ${r.title}`,
start: `${todayISO}T${r.time}`,
}))
);
const fcFontSizePx = ref(14);
const fcSizeVars = computed(() => ({
fontSize: `${fcFontSizePx.value}px`,
}));
const fcButtonVars = computed(() => ({
"--btn-px": "12px",
"--btn-py": "8px",
"--btn-h": "32px",
"--btn-font": "14px",
"--btn-radius": "5px",
"--btn-minw": "40px",
}));
const calendarOptions = computed(() => ({
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin],
locale: zhTw,
initialDate: todayISO,
initialView: "listWeek", //
height: "100%",
expandRows: true,
headerToolbar: {
left: "prev,next today",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay,listWeek",
},
selectable: false,
editable: false,
events: calendarEvents.value,
nowIndicator: true,
}));
const incidentRows = ref([
{ id: 1, time: "10:00", org: "崇恩護理之家", event: "跌倒事件" },
{ id: 2, time: "12:00", org: "育祐護理之家", event: "延遲給藥" },
{ id: 1, time: "10:00", org: "B 機構", event: "跌倒事件" },
{ id: 2, time: "12:00", org: "F 機構", event: "延遲給藥" },
]);
const incidentPageSize = 10;
const incidentPage = ref(1);
@ -1365,9 +1509,9 @@ const pagedIncidentRows = computed(() =>
);
const dispatchRows = ref([
{ id: 1, time: "09:30", org: "崇恩護理之家", contact: "黃國毅" },
{ id: 2, time: "10:30", org: "育祐護理之家", contact: "李佩怡" },
{ id: 3, time: "11:30", org: "崇智護理之家", contact: "陳筱安" },
{ id: 1, time: "09:30", org: "A 機構", contact: "黃國毅" },
{ id: 2, time: "10:30", org: "C 機構", contact: "李佩怡" },
{ id: 3, time: "11:30", org: "E 機構", contact: "陳筱安" },
]);
const dispatchPageSize = 10;
const dispatchPage = ref(1);
@ -1387,11 +1531,12 @@ const pagedDispatchRows = computed(() =>
function setInfoMode(mode) {
infoMode.value = mode;
nextTick(() => {
if (!map) return;
const m = map;
if (!m) return;
requestAnimationFrame(() => {
map.invalidateSize();
m?.invalidateSize?.();
requestAnimationFrame(() => {
map.invalidateSize();
m?.invalidateSize?.();
refreshAllTooltips();
});
});
@ -1399,4 +1544,82 @@ function setInfoMode(mode) {
}
</script>
<style scoped></style>
<style scoped>
/* 簡易轉場 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 去掉 hover/focus 的外框/陰影,改用變數控制底色 */
:deep(.fc .fc-button:hover),
:deep(.fc .fc-button:focus),
:deep(.fc .fc-button:focus-visible) {
box-shadow: none !important;
outline: none !important;
}
/* 小動畫更順眼 */
:deep(.fc .fc-button) {
transition: background-color 0.15s ease, border-color 0.15s ease,
color 0.15s ease;
}
/* 強化「被選取」按鈕current view以防某些主題覆蓋不到 */
:deep(.fc .fc-button.fc-button-active) {
background-color: var(--fc-button-active-bg-color) !important;
border-color: var(--fc-button-active-border-color) !important;
color: var(--fc-button-text-color) !important;
}
/* 基本按鈕尺寸:用自訂變數控制 */
:deep(.fc .fc-button) {
padding: var(--btn-py) var(--btn-px);
height: var(--btn-h);
font-size: var(--btn-font);
line-height: 1; /* 防止高度被行高撐開 */
border-radius: var(--btn-radius);
transition: background-color 0.15s ease, border-color 0.15s ease,
color 0.15s ease;
}
/* 視圖切換那一排(⽉/週/天/活動列表)統一寬度 */
:deep(.fc .fc-toolbar .fc-button-group .fc-button) {
min-width: var(--btn-minw);
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 上一週 / 下一週:做成正方形 icon 鈕 */
:deep(.fc .fc-prev-button),
:deep(.fc .fc-next-button) {
width: var(--btn-h);
padding: 0; /* 讓寬高精準btn-h */
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 調整箭頭圖示大小FullCalendar 內建 .fc-icon */
:deep(.fc .fc-button .fc-icon) {
font-size: var(--btn-icon);
line-height: 1;
}
/* 「今天」按鈕可自由跟其他一致(也能給不同 min-width */
:deep(.fc .fc-today-button) {
min-width: calc(var(--btn-minw) - 20px);
}
/* 先前做過:去掉 hover/focus 黑框 */
:deep(.fc .fc-button:hover),
:deep(.fc .fc-button:focus),
:deep(.fc .fc-button:focus-visible) {
box-shadow: none !important;
outline: none !important;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<section
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden text-brand-black"
class="flex flex-col gap-2 overflow-hidden text-brand-black"
>
<!-- 高度比重2 -->
<section class="flex-[2] grid grid-cols-3 gap-2">

View File

@ -1,6 +1,6 @@
<template>
<section
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden"
class="flex flex-col gap-2 overflow-hidden"
>
<!-- 高度比重2 -->
<div class="flex-[2] grid grid-cols-3 gap-2">

View File

@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;
@plugin "daisyui";
@import "@fullcalendar/core/index.css";
@import "@fullcalendar/daygrid/index.css";
@import "@fullcalendar/timegrid/index.css";
@import "@fullcalendar/list/index.css";
/* 匯入 Noto Sans TC 所有字重 */
@font-face {