feat: NavBart RWD 製作

This commit is contained in:
MJM_2025_05\polly 2025-09-02 11:31:17 +08:00
parent d1b51dcc9d
commit 458cfc7eb0
17 changed files with 995 additions and 131 deletions

View File

@ -13,7 +13,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>諾亞克 U-ARK 戰情中心</title>
</head>
<body class="w-screen bg-brand-gray-light">
<body class="w-screen bg-brand-gray-light font-noto text-brand-black">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

6
jsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"checkJs": false
},
"include": ["src/**/*.js", "src/**/*.vue", "src/types/**/*.d.ts"]
}

7
package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": {
"axios": "^1.11.0",
"echarts": "^6.0.0",
"leaflet": "^1.9.4",
"pinia": "^3.0.3",
"three": "^0.179.1",
"vue": "^3.5.18",
@ -2077,6 +2078,12 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",

View File

@ -11,6 +11,7 @@
"dependencies": {
"axios": "^1.11.0",
"echarts": "^6.0.0",
"leaflet": "^1.9.4",
"pinia": "^3.0.3",
"three": "^0.179.1",
"vue": "^3.5.18",

View File

Before

Width:  |  Height:  |  Size: 541 KiB

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View File

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

View File

@ -2,7 +2,7 @@
<section id="app" class="flex flex-col min-h-screen">
<NavBar class="fixed" />
<main class="w-full p-4 mt-[72px]">
<main class="w-full p-4">
<section class="grid grid-cols-7 gap-4 overflow-hidden">
<!-- 左側地圖 -->
<section class="col-span-3 bg-white/50 rounded-md shadow p-3 h-full overflow-hidden">

View File

@ -1,46 +1,48 @@
<template>
<nav
class="w-full h-[64px] bg-white bg-opacity-50 shadow-md flex justify-between items-center px-8"
class="sticky top-0 w-full h-14 md:h-16 bg-white/70 bg-opacity-80 shadow-md flex justify-between items-center px-4 sm:px-6 lg:px-8 z-[1000]"
>
<!-- 左側 logo選單區 -->
<div class="flex justify-start items-center gap-12">
<RouterLink to="/" class="h-[45px]">
<img src="/img/logo.png" alt="Logo" class="w-full h-full" />
<!-- 左側Logo + 機構切換 -->
<div
class="flex items-center gap-4 sm:gap-8 shrink-0 w-[240px] md:w-[300px] lg:w-[340px]"
>
<RouterLink to="/" class="h-9 md:h-11">
<img src="/img/logo.png" alt="Logo" class="h-full w-auto" />
</RouterLink>
<div class="relative">
<!-- 手機到桌機都顯示機構名稱維持桌機尺寸 -->
<div
ref="triggerRef"
class="btn text-white bg-brand-green hover:opacity-90 shadow-md border-none rounded-full px-8 tracking-widest"
class="btn bg-brand-green text-white hover:opacity-90 shadow-md border-none rounded-full px-6 lg:px-8 h-10 gap-2"
role="button"
tabindex="0"
aria-haspopup="true"
:aria-expanded="isOpen ? 'true' : 'false'"
:aria-expanded="isOpen"
@click="toggle"
@keydown.enter.prevent="toggle"
@keydown.space.prevent="toggle"
>
{{ displayLabel }}
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="m98 190.06l139.78 163.12a24 24 0 0 0 36.44 0L414 190.06c13.34-15.57 2.28-39.62-18.22-39.62h-279.6c-20.5 0-31.56 24.05-18.18 39.62"
/>
</svg>
</span>
<!-- 直接顯示不再隱藏 -->
<span>{{ displayLabel }}</span>
<!-- caret icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="m98 190.06l139.78 163.12a24 24 0 0 0 36.44 0L414 190.06c13.34-15.57 2.28-39.62-18.22-39.62h-279.6c-20.5 0-31.56 24.05-18.18 39.62"
/>
</svg>
</div>
<!-- Modal定位在按鈕右下方 -->
<!-- Dropdown小螢幕靠左桌機可改靠右 -->
<div
v-show="isOpen"
ref="panelRef"
class="absolute top-16 left-0 z-50 w-64 rounded-md border border-gray-100 bg-white shadow-lg p-2"
class="absolute top-12 left-0 md:top-12 md:left-0 lg:left-0 z-50 w-56 md:w-64 rounded-md border border-gray-100 bg-white shadow-lg p-2"
@click.stop
>
<ul class="max-h-48 overflow-y-auto text-brand-black">
@ -58,22 +60,20 @@
</div>
</div>
<!-- 中間 導覽按鈕區 -->
<!-- 中間導覽手機隱藏平板以上顯示桌機放大間距 -->
<div
class="min-w-[300px] text-brand-black bg-white shadow-md rounded-full grid grid-cols-3 items-center"
class="hidden md:grid min-w-[300px] grid-cols-3 items-center bg-white shadow-md rounded-full md:px-0 lg:min-w-[420px]"
>
<RouterLink to="/" v-slot="{ href, navigate, isExactActive }">
<a
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
'px-4 lg:px-6 py-2 rounded-full flex justify-center items-center transition-colors',
isExactActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>首頁</a
>
首頁
</a>
</RouterLink>
<RouterLink to="/operation" v-slot="{ href, navigate, isActive }">
@ -81,12 +81,11 @@
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
'px-4 lg:px-6 py-2 rounded-full flex justify-center items-center transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>營運</a
>
營運
</a>
</RouterLink>
<RouterLink to="/nursing" v-slot="{ href, navigate, isActive }">
@ -94,20 +93,21 @@
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
'px-4 lg:px-6 py-2 rounded-full flex justify-center items-center transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>照護</a
>
照護
</a>
</RouterLink>
</div>
<!-- 右側 登入區 -->
<div class="flex justify-end items-center gap-8">
<div
class="btn text-brand-black bg-white hover:opacity-90 shadow-md border-none rounded-full p-2 flex justify-center items-center"
<!-- 右側通知 + 使用者手機只顯示圖示平板顯示文字 -->
<div class="hidden md:flex items-center gap-3 sm:gap-6">
<button
class="btn bg-white text-brand-black hover:opacity-90 shadow-md border-none rounded-full p-2"
aria-label="通知"
>
<!-- 鈴鐺 -->
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@ -119,71 +119,262 @@
d="M8 2a4.5 4.5 0 0 0-4.5 4.5v2.401l-.964 2.414A.5.5 0 0 0 3 12h3c0 1.108.892 2 2 2s2-.892 2-2h3a.5.5 0 0 0 .464-.685L12.5 8.9V6.5A4.5 4.5 0 0 0 8 2m1 10c0 .556-.444 1-1 1s-1-.444-1-1zM4.5 6.5a3.5 3.5 0 1 1 7 0v2.498a.5.5 0 0 0 .036.185L12.262 11H3.738l.726-1.817a.5.5 0 0 0 .036-.185z"
/>
</svg>
</div>
</button>
<div
class="btn text-brand-black bg-white hover:opacity-90 shadow-md border-none rounded-full px-6 flex justify-center items-center gap-3"
class="btn bg-white text-brand-black hover:opacity-90 shadow-md border-none rounded-full px-3 md:px-5 h-9 md:h-10 gap-2 md:gap-3"
>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 md:w-5 md:h-5"
viewBox="0 0 16 16"
>
<path
fill="currentColor"
d="M11 7c0 1.66-1.34 3-3 3S5 8.66 5 7s1.34-3 3-3s3 1.34 3 3"
/>
<path
fill="currentColor"
fill-rule="evenodd"
d="M16 8c0 4.42-3.58 8-8 8s-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8M4 13.75C4.16 13.484 5.71 11 7.99 11c2.27 0 3.83 2.49 3.99 2.75A6.98 6.98 0 0 0 14.99 8c0-3.87-3.13-7-7-7s-7 3.13-7 7c0 2.38 1.19 4.49 3.01 5.75"
clip-rule="evenodd"
/>
</svg>
<p class="hidden md:inline">使用者</p>
<svg
xmlns="http://www.w3.org/2000/svg"
class="hidden md:block w-4 h-4"
viewBox="0 0 512 512"
>
<path
fill="currentColor"
d="m98 190.06l139.78 163.12a24 24 0 0 0 36.44 0L414 190.06c13.34-15.57 2.28-39.62-18.22-39.62h-279.6c-20.5 0-31.56 24.05-18.18 39.62"
/>
</svg>
</div>
</div>
<!-- 手機漢堡按鈕-->
<button
class="md:hidden btn bg-white text-brand-black hover:opacity-90 shadow-md border-none rounded-md p-2"
@click="toggleMobileMenu"
:aria-expanded="isMobileMenuOpen"
aria-controls="mobile-menu"
:aria-label="isMobileMenuOpen ? '關閉主選單' : '開啟主選單'"
>
<!-- 關閉(叉叉) -->
<svg
v-if="isMobileMenuOpen"
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m12 10.586l4.95-4.95l1.414 1.414L13.414 12l4.95 4.95l-1.414 1.414L12 13.414l-4.95 4.95l-1.414-1.414L10.586 12l-4.95-4.95l1.414-1.414z"
/>
</svg>
<!-- 漢堡 -->
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
>
<path fill="currentColor" d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z" />
</svg>
</button>
<!-- 手機全螢幕 ham menu白底hover active -->
<div
id="mobile-menu"
class="md:hidden fixed inset-0 z-[1100] bg-white p-4 flex flex-col gap-4 transition-opacity duration-200"
:class="
isMobileMenuOpen
? 'opacity-100 pointer-events-auto'
: 'opacity-0 pointer-events-none'
"
role="dialog"
aria-modal="true"
aria-label="主選單"
@keydown.esc.prevent="closeMobileMenu"
>
<div
v-show="isMobileMenuOpen"
id="mobile-menu"
class="md:hidden fixed inset-0 z-[1100] bg-white p-4 flex flex-col gap-4"
role="dialog"
aria-modal="true"
aria-label="主選單"
@keydown.esc.prevent="closeMobileMenu"
>
<!-- 頂部標題 + 叉叉和右上按鈕行為一致 -->
<div class="flex items-center justify-between">
<p class="font-semibold text-brand-black text-base">選單</p>
<button
class="btn btn-sm bg-white border-none shadow text-brand-black"
@click="closeMobileMenu"
aria-label="關閉主選單"
>
<path
fill="currentColor"
d="M11 7c0 1.66-1.34 3-3 3S5 8.66 5 7s1.34-3 3-3s3 1.34 3 3"
/>
<path
fill="currentColor"
fill-rule="evenodd"
d="M16 8c0 4.42-3.58 8-8 8s-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8M4 13.75C4.16 13.484 5.71 11 7.99 11c2.27 0 3.83 2.49 3.99 2.75A6.98 6.98 0 0 0 14.99 8c0-3.87-3.13-7-7-7s-7 3.13-7 7c0 2.38 1.19 4.49 3.01 5.75"
clip-rule="evenodd"
/>
</svg>
</span>
<p>使用者</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 512 512"
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m12 10.586l4.95-4.95l1.414 1.414L13.414 12l4.95 4.95l-1.414 1.414L12 13.414l-4.95 4.95l-1.414-1.414L10.586 12l-4.95-4.95l1.414-1.414z"
/>
</svg>
</button>
</div>
<!-- 導覽icon + 文字 -->
<nav class="space-y-2">
<RouterLink to="/" v-slot="{ href, navigate, isExactActive }">
<a
:href="href"
@click="
navigate;
closeMobileMenu();
"
:class="[
'flex items-center gap-3 rounded-lg px-3 py-3 transition-colors',
isExactActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4 19v-9q0-.475.213-.9t.587-.7l6-4.5q.525-.4 1.2-.4t1.2.4l6 4.5q.375.275.588.7T20 10v9q0 .825-.588 1.413T18 21h-3q-.425 0-.712-.288T14 20v-5q0-.425-.288-.712T13 14h-2q-.425 0-.712.288T10 15v5q0 .425-.288.713T9 21H6q-.825 0-1.412-.587T4 19"
/>
</svg>
<span class="text-brand-black">首頁</span>
</a>
</RouterLink>
<RouterLink to="/operation" v-slot="{ href, navigate, isActive }">
<a
:href="href"
@click="
navigate;
closeMobileMenu();
"
:class="[
'flex items-center gap-3 rounded-lg px-3 py-3 transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M20 13.75a.75.75 0 0 0-.75-.75h-3a.75.75 0 0 0-.75.75v6.75H14V4.25c0-.728-.002-1.2-.048-1.546c-.044-.325-.115-.427-.172-.484s-.159-.128-.484-.172C12.949 2.002 12.478 2 11.75 2s-1.2.002-1.546.048c-.325.044-.427.115-.484.172s-.128.159-.172.484c-.046.347-.048.818-.048 1.546V20.5H8V8.75A.75.75 0 0 0 7.25 8h-3a.75.75 0 0 0-.75.75V20.5H1.75a.75.75 0 0 0 0 1.5h20a.75.75 0 0 0 0-1.5H20z"
/>
</svg>
<span class="text-brand-black">營運</span>
</a>
</RouterLink>
<RouterLink to="/nursing" v-slot="{ href, navigate, isActive }">
<a
:href="href"
@click="
navigate;
closeMobileMenu();
"
:class="[
'flex items-center gap-3 rounded-lg px-3 py-3 transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M22 9.95v4.11a1.78 1.78 0 0 1-1.78 1.78h-4.39v4.39a1.73 1.73 0 0 1-.52 1.25a1.8 1.8 0 0 1-1.26.52H9.94a1.8 1.8 0 0 1-1.26-.52a1.8 1.8 0 0 1-.52-1.25v-4.39H3.78A1.78 1.78 0 0 1 2 14.06V9.95a1.78 1.78 0 0 1 1.78-1.78h4.38V3.78a1.8 1.8 0 0 1 1.103-1.646A1.8 1.8 0 0 1 9.94 2H14a1.8 1.8 0 0 1 1.26.52a1.77 1.77 0 0 1 .52 1.26v4.39h4.39c.472.003.924.19 1.26.52A1.78 1.78 0 0 1 22 9.95"
/>
</svg>
<span class="text-brand-black">照護</span>
</a>
</RouterLink>
</nav>
<hr class="border-gray-200" />
<!-- 通知 + 使用者放在全螢幕 menu 同樣 hover active -->
<div class="space-y-2">
<button
class="w-full flex items-center gap-3 rounded-lg px-3 py-3 transition-colors hover:bg-gray-100 text-left"
@click="closeMobileMenu()"
>
<path
fill="currentColor"
d="m98 190.06l139.78 163.12a24 24 0 0 0 36.44 0L414 190.06c13.34-15.57 2.28-39.62-18.22-39.62h-279.6c-20.5 0-31.56 24.05-18.18 39.62"
/>
</svg>
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
>
<path
fill="currentColor"
d="M12.45 16.002a2.5 2.5 0 0 1-4.9 0zM9.998 2c3.149 0 5.744 2.335 5.984 5.355l.013.223l.005.224l-.001 3.606l.954 2.587l.025.085l.016.086l.005.089c0 .315-.196.59-.522.707l-.114.033l-.114.01H3.751a.8.8 0 0 1-.259-.047c-.287-.105-.476-.372-.482-.716l.004-.117l.034-.13l.95-2.584L4 7.793l.004-.225C4.127 4.451 6.771 2 9.998 2"
/>
</svg>
<span class="text-brand-black">通知</span>
</button>
<button
class="w-full flex items-center gap-3 rounded-lg px-3 py-3 transition-colors hover:bg-gray-100 text-left"
@click="closeMobileMenu()"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M7 7a5 5 0 1 1 10 0A5 5 0 0 1 7 7M3.5 19a5 5 0 0 1 5-5h7a5 5 0 0 1 5 5v2h-17z"
/>
</svg>
<span class="text-brand-black">使用者</span>
</button>
</div>
<div class="mt-auto text-xs text-gray-500">© U-ARK</div>
</div>
</div>
</nav>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, defineOptions } from "vue";
// modal
const selectItem = (item) => {
selectedItem.value = item;
isOpen.value = false;
};
// >4
const displayLabel = computed(() => {
const label = String(selectedItem.value ?? "護祐");
return label.length > 4 ? label.slice(0, 4) + "..." : label;
});
defineOptions({ name: "NavBar" });
import {
ref,
computed,
onMounted,
onUnmounted,
defineOptions,
nextTick,
} from "vue";
// ====== Dropdown======
const isOpen = ref(false);
const triggerRef = ref(null);
const panelRef = ref(null);
//
const facilities = [
"總部",
"護祐護理之家",
@ -200,16 +391,22 @@ const facilities = [
"崇智護理之家",
"慈祐長照中心(養護型)",
];
// li
const selectedItem = ref(facilities[0]);
// /
const selectItem = (item) => {
selectedItem.value = item;
isOpen.value = false;
};
const displayLabel = computed(() => {
const label = String(selectedItem.value ?? "護祐");
return label.length > 4 ? label.slice(0, 4) + "..." : label;
});
defineOptions({ name: "NavBar" });
const toggle = () => {
isOpen.value = !isOpen.value;
};
//
const onClickOutside = (e) => {
const t = e.target;
if (!triggerRef.value || !panelRef.value) return;
@ -220,19 +417,35 @@ const onClickOutside = (e) => {
}
};
// Esc
const onKeydown = (e) => {
if (e.key === "Escape") isOpen.value = false;
// ====== ======
const isMobileMenuOpen = ref(false);
const toggleMobileMenu = async () => {
isMobileMenuOpen.value = !isMobileMenuOpen.value;
if (isMobileMenuOpen.value) {
await nextTick();
//
}
};
const closeMobileMenu = () => {
isMobileMenuOpen.value = false;
};
// ====== Esc ======
const handleKeydown = (e) => {
if (e.key === "Escape") {
isOpen.value = false; // dropdown
isMobileMenuOpen.value = false; //
}
};
onMounted(() => {
document.addEventListener("click", onClickOutside);
document.addEventListener("keydown", onKeydown);
document.addEventListener("keydown", handleKeydown); //
});
onUnmounted(() => {
document.removeEventListener("click", onClickOutside);
document.removeEventListener("keydown", onKeydown);
document.removeEventListener("keydown", handleKeydown); //
});
</script>

View File

@ -2,7 +2,8 @@ import { createApp } from 'vue'
import { createPinia } from "pinia";
import './style.css'
import App from './App.vue'
import router from './router'
import router from './router'
import 'leaflet/dist/leaflet.css';
const app = createApp(App)

View File

@ -21,7 +21,7 @@
></progress>
<span
class="pointer-events-none absolute inset-y-0 flex items-center text-[20px] font-nats text-brand-black/80"
class="pointer-events-none absolute bottom-0 flex items-center text-[20px] font-nats text-brand-black/80"
:class="textPosClass"
>
{{ currentLocale }} / {{ totalLocale }}

View File

@ -1,22 +1,22 @@
<template>
<section
class="grid grid-cols-3 gap-2 h-[calc(100vh-72px-32px)] justify-center text-brand-black"
class="grid grid-cols-3 gap-2 h-[calc(100vh-72px-32px)] justify-center"
>
<!-- 左側 -->
<section class="grid grid-rows-12 gap-2">
<!-- 機構照片 + 機構說明 + 核心資料 bars + chart -->
<section
class="row-span-10 bg-white/50 rounded-md shadow py-6 px-4 flex flex-col items-start gap-4"
class="h-[740px] row-span-9 bg-white/50 rounded-md shadow py-6 px-4 flex flex-col items-start gap-4"
>
<div
class="grid grid-cols-12 justify-center items-start gap-8 "
class="w-full h-[400px] grid grid-cols-2 justify-center items-start gap-6 px-2"
>
<div
class="col-span-5 grid grid-rows-12 justify-start items-start gap-6"
class="w-full col-span-1 grid grid-rows-12 justify-start items-start gap-6"
>
<div class="h-[240px] row-span-6">
<img
src="/img/building_photo_headquarter.png"
src="/img/building/headquarter.png"
alt="機構照片"
class="w-full h-full rounded-md object-cover"
/>
@ -28,61 +28,688 @@
</div>
</div>
<!-- Progress bars -->
<div class="col-span-6 flex flex-col gap-4">
<div class="w-full col-span-1 flex flex-col gap-6">
<ProgressBar
label="現在住民/全立案床數"
:current="240"
:total="360"
/>
<ProgressBar
label="目前空床數/全立案床數"
:current="120"
:total="360"
/>
<ProgressBar
label="今日住院/當月累積住院"
:current="8"
:total="50"
/>
<ProgressBar
label="今日新人入住/當月累積入住"
:current="12"
:total="50"
/>
<ProgressBar
label="今日離院/累積離院"
:current="8"
:total="50"
/>
<ProgressBar label="今日離院/累積離院" :current="8" :total="50" />
</div>
</div>
<div class="bg-black">
<div class="w-full h-[240px] flex justify-center items-center">
<!-- chart -->
<div ref="chartEl" class="w-full h-full"></div>
</div>
</section>
<!-- 素資料 -->
<section class="row-span-2 grid grid-cols-2 gap-2">
<div class="col-span-1 bg-white/50 rounded-md shadow p-3"></div>
<div class="col-span-1 bg-white/50 rounded-md shadow p-3"></div>
<section class="row-span-3 grid grid-cols-2 gap-2">
<div class="col-span-1 bg-white/50 rounded-md shadow p-6">
<div
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-start items-center gap-3 p-3"
>
<div class="relative text-brand-yellow">
<svg
xmlns="http://www.w3.org/2000/svg"
width="90"
height="90"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M13 2.05v3.03c3.39.49 6 3.39 6 6.92c0 .9-.18 1.75-.5 2.54l2.62 1.53c.56-1.24.88-2.62.88-4.07c0-5.18-3.95-9.45-9-9.95M12 19a7 7 0 0 1-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95a10 10 0 0 0 10 10c3.3 0 6.23-1.61 8.05-4.09l-2.6-1.53A6.89 6.89 0 0 1 12 19"
/>
</svg>
<p
class="w-9 h-9 flex justify-center items-center p-2 bg-brand-yellow text-brand-black absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
>
</p>
</div>
<div class="flex gap-6">
<div class="flex flex-col justify-center items-start gap-3">
<p>總數100</p>
<p>一般90</p>
</div>
<div class="flex flex-col justify-center items-start gap-3">
<p>碎食5</p>
<p>管灌5</p>
</div>
</div>
</div>
</div>
<div class="col-span-1 bg-white/50 rounded-md shadow p-6">
<div
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-start items-center gap-3 p-3"
>
<div class="relative text-brand-yellow">
<svg
xmlns="http://www.w3.org/2000/svg"
width="90"
height="90"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M13 2.05v3.03c3.39.49 6 3.39 6 6.92c0 .9-.18 1.75-.5 2.54l2.62 1.53c.56-1.24.88-2.62.88-4.07c0-5.18-3.95-9.45-9-9.95M12 19a7 7 0 0 1-7-7c0-3.53 2.61-6.43 6-6.92V2.05c-5.06.5-9 4.76-9 9.95a10 10 0 0 0 10 10c3.3 0 6.23-1.61 8.05-4.09l-2.6-1.53A6.89 6.89 0 0 1 12 19"
/>
</svg>
<p
class="w-9 h-9 flex justify-center items-center p-2 bg-brand-yellow text-brand-black absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
>
</p>
</div>
<div class="flex gap-6">
<div class="flex flex-col justify-center items-start gap-3">
<p>總數100</p>
<p>一般90</p>
</div>
<div class="flex flex-col justify-center items-start gap-3">
<p>碎食5</p>
<p>管灌5</p>
</div>
</div>
</div>
</div>
</section>
</section>
<!-- 中間 -->
<section class="bg-white/50 rounded-md shadow p-3"></section>
<section class="bg-white/50 rounded-md shadow p-3">
<div ref="mapEl" class="w-full h-full rounded-md overflow-hidden"></div>
</section>
<!-- 右側 -->
<section class="bg-white/50 rounded-md shadow p-3"></section>
<section class="bg-white/50 rounded-md shadow p-3 flex flex-col gap-2">
<!-- 表格 今日活動 -->
<section class="rounded-md shadow p-3 flex flex-col min-h-0 gap-3">
<h3 class="text-xl 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">
<thead
class="bg-brand-gray-light 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] text-center">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in pagedRows"
:key="row.id"
class="transition-colors duration-150 hover:bg-brand-gray-light focus-visible:bg-gray-100/70"
>
<td>{{ row.time }}</td>
<td>{{ row.org }}</td>
<td class="truncate">{{ row.title }}</td>
<td class="text-center">
<button
class="btn btn-link btn-xs px-0 text-brand-purple-dark !no-underline hover:!no-underline hover:opacity-80 focus-visible:!no-underline"
@click="viewDetail(row)"
>
查看詳情
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分頁 -->
<div class="mt-3 flex items-center justify-between px-3">
<span class="text-sm text-gray-500">
{{ total }} 每頁 {{ pageSize }}
</span>
<div class="inline-flex items-center gap-2">
<button
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一頁
</button>
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一頁
</button>
</div>
</div>
</section>
<!-- 表格 今日異常事件 -->
<section class="rounded-md shadow p-3 flex flex-col min-h-0 gap-3">
<h3 class="text-xl 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">
<thead
class="bg-brand-gray-light text-brand-black sticky top-0 z-[1]"
>
<tr>
<th class="w-[100px]">時間</th>
<th class="w-[160px]">機構</th>
<th class="w-[180px]">事件</th>
<th class="w-[120px] text-center">查看詳情</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in pagedIncidentRows"
:key="row.id"
class="transition-colors duration-150 hover:bg-brand-gray-light focus-visible:bg-gray-100/70"
>
<td>{{ row.time }}</td>
<td class="truncate" :title="row.org">{{ row.org }}</td>
<td class="truncate" :title="row.event">{{ row.event }}</td>
<td class="text-center">
<button
class="btn btn-link btn-xs px-0 text-brand-purple-dark !no-underline hover:!no-underline hover:opacity-80 focus-visible:!no-underline"
@click="viewDetailIncident(row)"
>
查看詳情
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分頁固定在底 -->
<div class="mt-3 flex items-center justify-between">
<span class="ml-3 text-sm text-gray-500">
{{ incidentTotal }} 每頁 {{ incidentPageSize }}
</span>
<div class="inline-flex items-center gap-2">
<button
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一頁
</button>
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一頁
</button>
</div>
</div>
</section>
<!-- 表格 今日派車總表 -->
<section class="rounded-md shadow p-3 flex flex-col min-h-0 gap-3">
<h3 class="text-xl font-bold">今日派車總表</h3>
<!-- 可捲動內容區水平 -->
<div class="flex flex-col gap-4 mb-6">
<div class="w-full overflow-x-auto">
<table class="table whitespace-nowrap">
<thead
class="bg-brand-gray-light text-brand-black sticky top-0 z-[1]"
>
<tr>
<th class="w-[120px]">時間</th>
<th class="w-[160px]">機構</th>
<th class="w-[120px]">聯絡人</th>
<th class="w-[120px] text-center">操作</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in pagedDispatchRows"
:key="row.id"
class="transition-colors duration-150 hover:bg-brand-gray-light focus-visible:bg-gray-100/70"
>
<td>{{ row.time }}</td>
<td class="truncate" :title="row.org">{{ row.org }}</td>
<td class="truncate" :title="row.contact">
{{ row.contact }}
</td>
<td class="text-center">
<button
class="btn btn-link btn-xs px-0 text-brand-purple-dark !no-underline hover:!no-underline hover:opacity-80 focus-visible:!no-underline"
@click="viewDispatchDetail(row)"
>
查看詳情
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分頁固定在底 -->
<div class="mt-3 flex items-center justify-between">
<span class="ml-3 text-sm text-gray-500">
{{ dispatchTotal }} 每頁 {{ dispatchPageSize }}
</span>
<div class="inline-flex items-center">
<button
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一頁
</button>
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
下一頁
</button>
</div>
</div>
</section>
</section>
</section>
</template>
<script setup>
import ProgressBar from "./ProgressBar.vue";
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import * as echarts from "echarts";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { brand } from "@/styles/palette";
// 7 M/D
function last7DaysLabels() {
const labels = [];
const now = new Date();
for (let i = 6; i >= 0; i--) {
const d = new Date(now);
d.setDate(now.getDate() - i);
labels.push(`${d.getMonth() + 1}/${d.getDate()}`);
}
return labels;
}
// Demo 0~100
const dataA = [15, 58, 25, 75, 90, 38, 76];
const dataB = [26, 78, 85, 32, 30, 52, 72];
const chartEl = ref(null);
let chart;
onMounted(() => {
if (!chartEl.value) return;
chart = echarts.init(chartEl.value);
const labels = last7DaysLabels();
chart.setOption({
color: [brand.green, brand.purple], //
tooltip: {
trigger: "axis",
axisPointer: { type: "line" },
confine: true,
},
legend: {
bottom: 8,
icon: "circle",
itemWidth: 10,
itemHeight: 10,
itemGap: 24,
textStyle: { color: brand.gray },
data: ["現在住民", "全立案人數"],
},
grid: { left: 36, right: 16, top: 16, bottom: 48, containLabel: true },
xAxis: {
type: "category",
boundaryGap: false,
data: labels,
axisLine: { lineStyle: { color: brand.gray } },
axisTick: { show: false },
axisLabel: { color: brand.gray },
splitLine: { show: false },
},
yAxis: {
type: "value",
min: 0,
max: 100,
interval: 20,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { color: brand.gray },
splitLine: {
show: true,
lineStyle: { color: brand.grayLight },
},
},
series: [
{
name: "現在住民",
type: "line",
data: dataA,
symbol: "circle",
symbolSize: 6,
lineStyle: { width: 2 },
itemStyle: { opacity: 1 },
},
{
name: "全立案人數",
type: "line",
data: dataB,
symbol: "circle",
symbolSize: 6,
lineStyle: { width: 2 },
itemStyle: { opacity: 1 },
},
],
animationDuration: 500,
});
// RWD
const onResize = () => chart?.resize();
window.addEventListener("resize", onResize);
chart.__onResize = onResize;
});
onBeforeUnmount(() => {
if (chart) {
window.removeEventListener("resize", chart.__onResize);
chart.dispose();
chart = null;
}
});
// BASE_URL
const base = import.meta.env.BASE_URL;
const iconBase = `${base}img/leaflet/`;
const defaultIcon = L.icon({
iconUrl: `${iconBase}marker-icon.png`,
iconRetinaUrl: `${iconBase}marker-icon-2x.png`,
shadowUrl: `${iconBase}marker-shadow.png`,
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41],
});
L.Marker.prototype.options.icon = defaultIcon;
const mapEl = ref(null);
let map = null;
// 2)
const KAOHSIUNG_BOUNDS_TIGHT = L.latLngBounds(
[22.45, 120.15], // SW
[22.95, 120.55] // NE
);
const KAOHSIUNG_BOUNDS_LOOSE = L.latLngBounds(
[22.35, 120.0], // SW
[23.05, 120.75] // NE
);
// 3) fetch
const LOCATIONS = [
{
name: "崇恩護理之家",
addr: "高雄市楠梓區立仁街131、133號",
lat: 22.7409648895,
lng: 120.3354644775,
},
{
name: "育祐護理之家",
addr: "高雄市楠梓區常德路317巷9弄27號",
lat: 22.7366924286,
lng: 120.3363342285,
},
{
name: "崇祐護理之家",
addr: "高雄市楠梓區宏昌街135、137號",
lat: 22.717588,
lng: 120.29406,
},
{
name: "崇智護理之家",
addr: "高雄市左營區民族一路980號",
lat: 22.678054,
lng: 120.3192,
},
{
name: "護祐護理之家",
addr: "高雄市三民區黃興路336號",
lat: 22.651488,
lng: 120.33731,
},
{
name: "傳祐長照中心",
addr: "高雄市小港區沿海一路377號",
lat: 22.5644512177,
lng: 120.3544540405,
},
];
onMounted(() => {
if (!mapEl.value) return;
map = L.map(mapEl.value, {
center: [22.6273, 120.3014],
zoom: 13,
minZoom: 12,
maxZoom: 20,
zoomSnap: 0.25,
zoomDelta: 0.25,
maxBounds: KAOHSIUNG_BOUNDS_LOOSE,
maxBoundsViscosity: 0.8,
preferCanvas: true,
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 20,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
const ICON_H = defaultIcon.options.iconSize?.[1] ?? 41; // marker
const TIP_GAP = 8; // pin tooltip
// featureGroup 便調
const group = L.featureGroup();
LOCATIONS.forEach((p) => {
L.marker([p.lat, p.lng])
.addTo(group)
.bindTooltip(
`<div class="tip p-2">
<div class="flex flex-col gap-2">
<!-- 第一列icon + 名稱-->
<div class="inline-flex justify-start items-center text-brand-purple-dark font-noto gap-1">
<span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M5 19v-8.692q0-.384.172-.727t.474-.565l5.385-4.078q.423-.323.966-.323t.972.323l5.385 4.077q.303.222.474.566q.172.343.172.727V19q0 .402-.299.701T18 20h-3.384q-.344 0-.576-.232q-.232-.233-.232-.576v-4.769q0-.343-.232-.575q-.233-.233-.576-.233h-2q-.343 0-.575.233q-.233.232-.233.575v4.77q0 .343-.232.575T9.385 20H6q-.402 0-.701-.299T5 19"/>
</svg>
</span>
<strong class="text-[14px]">${p.name}</strong>
</div>
<!-- 第二列統計卡群 -->
<div class="flex justify-center items-center gap-1">
<div class="flex flex-col justify-center items-center gap-1 border p-2 rounded-md">
<div class="text-[16px]"><strong>住民</strong></div>
<div class="font-nats text-xl text-brand-purple-dark"><p>36 / 49</p></div>
</div>
<div class="flex flex-col justify-center items-center gap-1 border p-2 rounded-md">
<div class="text-[16px]"><strong>空床</strong></div>
<div class="font-nats text-xl text-brand-purple-dark"><p>12 / 49</p></div>
</div>
<div class="flex flex-col justify-center items-center gap-1 border p-2 rounded-md">
<div class="text-[16px]"><strong>住院</strong></div>
<div class="font-nats text-xl text-brand-purple-dark"><p>1 / 20</p></div>
</div>
</div>
</div>
</div>
`,
{
permanent: true, //
direction: "top", // pin
offset: [0, -(ICON_H + TIP_GAP)],
opacity: 0.9,
}
);
});
group.addTo(map);
const bounds = group.getBounds().pad(0.06);
map.fitBounds(bounds, {
paddingTopLeft: [24, 140],
paddingBottomRight: [24, 24],
maxZoom: 15,
});
});
onBeforeUnmount(() => {
if (map) {
map.remove();
map = null;
}
});
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: "消防夜間演練",
},
]);
//
const pageSize = 10;
const currentPage = ref(1);
const total = computed(() => rows.value.length);
const totalPages = computed(() =>
Math.max(1, Math.ceil(total.value / pageSize))
);
const pagedRows = computed(() => {
const start = (currentPage.value - 1) * pageSize;
return rows.value.slice(start, start + pageSize);
});
//
function viewDetail(row) {
console.log("查看詳情:", row);
}
const incidentRows = ref([
{
id: 1,
time: "10:00",
org: "崇恩護理之家",
event: "跌倒事件",
},
{
id: 2,
time: "12:00",
event: "延遲給藥",
org: "育祐護理之家",
},
]);
//
const incidentPageSize = 10;
const incidentPage = ref(1);
const incidentTotal = computed(() => incidentRows.value.length);
const incidentTotalPages = computed(() =>
Math.max(1, Math.ceil(incidentTotal.value / incidentPageSize))
);
const pagedIncidentRows = computed(() => {
const start = (incidentPage.value - 1) * incidentPageSize;
return incidentRows.value.slice(start, start + incidentPageSize);
});
function viewDetailIncident(row) {
console.log("事件詳情:", row);
}
//
const dispatchRows = ref([
{ id: 1, time: "09:30", org: "崇恩護理之家", contact: "黃國毅" },
{ id: 2, time: "10:30", org: "育祐護理之家", contact: "李佩怡" },
{ id: 3, time: "11:30", org: "崇智護理之家", contact: "陳筱安" },
]);
//
const dispatchPageSize = 10;
const dispatchPage = ref(1);
const dispatchTotal = computed(() => dispatchRows.value.length);
const dispatchTotalPages = computed(() =>
Math.max(1, Math.ceil(dispatchTotal.value / dispatchPageSize))
);
const pagedDispatchRows = computed(() => {
const start = (dispatchPage.value - 1) * dispatchPageSize;
return dispatchRows.value.slice(start, start + dispatchPageSize);
});
//
function viewDispatchDetail(row) {
console.log("派車詳情:", row);
}
</script>
<style lang="scss" scoped></style>
<style scoped></style>

View File

@ -157,3 +157,10 @@ body {
overflow-x: hidden; /* 防止偶發橫向卷軸 */
font-family: "Noto Sans TC", sans-serif;
}
/* 降低 Leaflet 控制層級:預設是 1000 */
.leaflet-top,
.leaflet-bottom {
z-index: 400 !important; /* 低於你的 Nav但仍高於地圖瓦片 */
}

View File

@ -3,6 +3,7 @@
export const brand = {
green: "#34D5C8",
greenLight: "#C4FBE5",
greenDark:"#0CA99C",
red: "#FF8678",
purple: "#A5BEFF",
purpleLight: "#D5E1FF",
@ -10,5 +11,6 @@ export const brand = {
yellow: "#E1F391",
black: "#424242",
gray: "#828282",
grayLight: "#F0F0F0",
grayLight: "#E9E9E9",
grayDark: "#D2D2D2",
};

View File

@ -40,7 +40,7 @@ module.exports = {
},
colors: {
brand: {
green: { DEFAULT: "#34D5C8", light: "#C4FBE5" },
green: { DEFAULT: "#34D5C8", light: "#C4FBE5", dark:"#0CA99C" },
red: "#FF8678",
purple: {
DEFAULT: "#A5BEFF",
@ -53,9 +53,9 @@ module.exports = {
},
black: "#424242",
gray: {
light: "#F0F0F0",
deepLight:"#D2D2D2",
DEFAULT: "#828282",
light: "#E9E9E9",
dark: "#D2D2D2",
},
},
},