feat: 新增大戰情首頁

This commit is contained in:
MJM_2025_05\polly 2025-08-29 17:58:16 +08:00
parent cb74fdb0f2
commit d1b51dcc9d
17 changed files with 323 additions and 1545 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-neutral">
<body class="w-screen bg-brand-gray-light">
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

View File

@ -1,42 +1,23 @@
<template>
<section id="app" class="flex flex-col min-h-screen">
<NavBar />
<!-- 兩欄 Grid各佔 50% -->
<section class="w-full px-4 py-4">
<div
class="grid grid-cols-7 gap-4 h-[calc(100vh-72px-32px)] overflow-hidden"
>
<!-- 左側地圖區 -->
<section
class="col-span-3 bg-white bg-opacity-50 rounded-xl shadow p-3 h-full overflow-hidden"
>
<div
class="w-full h-full flex items-center justify-center text-gray-500"
>
<Forge />
</div>
</section>
<!-- 右側路由切換內容預留不滾動 -->
<main class="col-span-4 h-full overflow-hidden">
<div class="w-full h-full">
<!-- 右側內容會依路由切換Home / Operation / Nursing / SocialWorker / Nutrition -->
<RouterView />
</div>
</main>
</div>
</section>
</section>
<!-- 依據 route.meta.layout 動態載入對應 Layout -->
<component :is="layoutComponent" />
</template>
<script>
import NavBar from "@/layouts/NavBar.vue";
import Forge from "@/components/forge/Forge.vue";
// import ForgeForSystem from "@/components/forge/ForgeForSystem.vue";
<script setup>
import { computed, defineAsyncComponent } from "vue";
import { useRoute } from "vue-router";
export default {
name: "App",
components: { NavBar, Forge },
// Layout
const layouts = {
headquarter: defineAsyncComponent(() => import("@/layouts/HeadquarterLayout.vue")),
map: defineAsyncComponent(() => import("@/layouts/MapLayout.vue")),
};
const route = useRoute();
// 使 map meta.layout
const layoutComponent = computed(() => {
const key = route.meta?.layout ?? "map";
return layouts[key] ?? layouts.map;
});
</script>

View File

@ -587,7 +587,7 @@ onUnmounted(() => {
@click="bringToFrontById(L.id)"
>
<div
class="pointer-events-auto relative bg-white/95 border border-gray-300 rounded-lg shadow px-3 py-2 text-sm cursor-pointer"
class="pointer-events-auto relative bg-white/95 border border-gray-300 rounded-md shadow px-3 py-2 text-sm cursor-pointer"
style="will-change: transform"
@click.stop="openResidentModal(L, $event)"
role="button"

View File

@ -0,0 +1,12 @@
<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">
<RouterView />
</main>
</section>
</template>
<script setup>
import NavBar from "@/layouts/NavBar.vue";
</script>

30
src/layouts/MapLayout.vue Normal file
View File

@ -0,0 +1,30 @@
<template>
<section id="app" class="flex flex-col min-h-screen">
<NavBar class="fixed" />
<main class="w-full p-4 mt-[72px]">
<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">
<div class="w-full h-full flex items-center justify-center text-gray-500">
<Forge />
</div>
</section>
<!-- 右側路由內容 -->
<section class="col-span-4 h-full overflow-hidden">
<div class="w-full h-full">
<RouterView />
</div>
</section>
</section>
</main>
</section>
</template>
<script setup>
import NavBar from "@/layouts/NavBar.vue";
//
import { defineAsyncComponent } from "vue";
const Forge = defineAsyncComponent(() => import("@/components/forge/Forge.vue"));
</script>

View File

@ -1,6 +1,6 @@
<template>
<nav
class="w-full h-[72px] bg-white bg-opacity-50 shadow-md flex justify-between items-center px-8"
class="w-full h-[64px] bg-white bg-opacity-50 shadow-md flex justify-between items-center px-8"
>
<!-- 左側 logo選單區 -->
<div class="flex justify-start items-center gap-12">
@ -9,7 +9,6 @@
</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"
@ -37,11 +36,11 @@
</span>
</div>
<!-- Modal定位在按鈕右下方 top-8 left-16 -->
<!-- Modal定位在按鈕右下方 -->
<div
v-show="isOpen"
ref="panelRef"
class="absolute top-16 left-0 z-50 w-64 rounded-xl border border-gray-100 bg-white shadow-lg p-2"
class="absolute top-16 left-0 z-50 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">
@ -60,75 +59,48 @@
</div>
<!-- 中間 導覽按鈕區 -->
<div>
<div
class="w-[450px] text-brand-black bg-white shadow-md rounded-full grid grid-cols-5 items-center"
>
<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',
isExactActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
首頁
</a>
</RouterLink>
<RouterLink to="/operation" v-slot="{ href, navigate, isActive }">
<a
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
營運
</a>
</RouterLink>
<div
class="min-w-[300px] text-brand-black bg-white shadow-md rounded-full grid grid-cols-3 items-center"
>
<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',
isExactActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
首頁
</a>
</RouterLink>
<RouterLink to="/nursing" v-slot="{ href, navigate, isActive }">
<a
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
護理
</a>
</RouterLink>
<RouterLink to="/operation" v-slot="{ href, navigate, isActive }">
<a
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
營運
</a>
</RouterLink>
<RouterLink to="/social-worker" v-slot="{ href, navigate, isActive }">
<a
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
社工
</a>
</RouterLink>
<RouterLink to="/nutrition" v-slot="{ href, navigate, isActive }">
<a
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
營養
</a>
</RouterLink>
</div>
<RouterLink to="/nursing" v-slot="{ href, navigate, isActive }">
<a
:href="href"
@click="navigate"
:class="[
'px-5 py-2 rounded-full flex justify-center items-center transition-colors',
isActive ? 'bg-brand-green-light' : 'hover:bg-gray-100',
]"
>
照護
</a>
</RouterLink>
</div>
<!-- 右側 登入區 -->
@ -213,6 +185,7 @@ const panelRef = ref(null);
//
const facilities = [
"總部",
"護祐護理之家",
"崇祐護理之家",
"崇祐長照中心(養護型)",

View File

@ -0,0 +1,60 @@
<template>
<div class="space-y-2">
<!-- 標題列可選 icon 插槽 -->
<div class="flex items-center gap-2">
<label class="block font-medium">{{ label }}</label>
<slot name="icon" />
</div>
<!-- 進度條文字覆蓋在進度條上預設靠左 -->
<div class="relative">
<progress
v-bind="$attrs"
class="progress w-full h-5 bg-brand-gray-light text-brand-green-light text-left rounded-none [&::-webkit-progress-bar]:rounded-none [&::-webkit-progress-value]:rounded-none [&::-moz-progress-bar]:rounded-none"
:class="heightClass"
:value="current"
:max="total"
role="progressbar"
:aria-valuenow="current"
aria-valuemin="0"
:aria-valuemax="total"
></progress>
<span
class="pointer-events-none absolute inset-y-0 flex items-center text-[20px] font-nats text-brand-black/80"
:class="textPosClass"
>
{{ currentLocale }} / {{ totalLocale }}
</span>
</div>
</div>
</template>
<script setup>
import { computed } from "vue";
defineOptions({ inheritAttrs: false });
const props = defineProps({
label: { type: String, required: true },
current: { type: Number, required: true },
total: { type: Number, required: true },
textAlign: { type: String, default: "left" }, // left | center | right
});
const heightClass = computed(() => `h-${props.height}`);
const textPosClass = computed(() => {
switch (props.textAlign) {
case "center":
return "left-1/2 -translate-x-1/2";
case "right":
return "right-2";
default:
return "left-2";
}
});
const currentLocale = computed(() => props.current.toLocaleString());
const totalLocale = computed(() => props.total.toLocaleString());
</script>

View File

@ -1,631 +1,88 @@
<template>
<section
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden"
class="grid grid-cols-3 gap-2 h-[calc(100vh-72px-32px)] justify-center text-brand-black"
>
<!-- 高度比重2 -->
<div class="flex-[2] grid grid-cols-4 gap-2">
<!-- 住民人數 Card -->
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
@click="openModal('residents')"
<!-- 左側 -->
<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"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">住民人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">36/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<!-- 住院人數 Card -->
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
@click="openModal('inpatients')"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">住院人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">1/36</p>
<p class="text-[12px]"></p>
</div>
</div>
<!-- ===== Modal放在同一個 <template> 建議貼在最底部===== -->
<div
v-if="showModal"
class="fixed inset-0 z-[100] flex items-center justify-center"
@keydown.esc="closeModal"
tabindex="0"
>
<!-- backdrop -->
<div class="absolute inset-0 bg-black/40" @click="closeModal"></div>
<!-- dialog -->
<div
class="relative bg-white w-[92vw] max-w-5xl max-h-[86vh] overflow-auto rounded-2xl shadow-xl px-12 py-8"
role="dialog"
aria-modal="true"
class="grid grid-cols-12 justify-center items-start gap-8 "
>
<!-- header -->
<div class="flex items-center justify-between mb-4">
<h3 class="text-2xl font-bold text-gray-800">
{{ modalTitle }}
</h3>
<button
class="p-2 rounded hover:bg-gray-100"
@click="closeModal"
aria-label="關閉"
>
</button>
</div>
<!-- body住民資訊A/B + 表格 -->
<div v-if="modalType === 'residents'" class="space-y-8">
<section>
<h4 class="text-lg font-semibold text-gray-700 mb-3">A </h4>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-brand-black">
<thead>
<tr class="text-left bg-gray-50">
<th class="px-3 py-2">床位</th>
<th class="px-3 py-2">姓名</th>
<th class="px-3 py-2">性別</th>
<th class="px-3 py-2">年齡</th>
<th class="px-3 py-2">身體狀況</th>
<th class="px-3 py-2">備註</th>
</tr>
</thead>
<tbody>
<tr
v-for="r in residentsA"
:key="r.bed"
class="border-b last:border-b-0 hover:bg-gray-50"
>
<td class="px-3 py-2 font-mono">{{ r.bed }}</td>
<td class="px-3 py-2">{{ r.name }}</td>
<td class="px-3 py-2">{{ r.gender }}</td>
<td class="px-3 py-2">{{ r.age }}</td>
<td class="px-3 py-2">{{ r.condition }}</td>
<td class="px-3 py-2">{{ r.note }}</td>
</tr>
</tbody>
</table>
</div>
</section>
<section>
<h4 class="text-lg font-semibold text-gray-700 mb-3">B </h4>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-brand-black">
<thead>
<tr class="text-left bg-gray-50">
<th class="px-3 py-2">床位</th>
<th class="px-3 py-2">姓名</th>
<th class="px-3 py-2">性別</th>
<th class="px-3 py-2">年齡</th>
<th class="px-3 py-2">身體狀況</th>
<th class="px-3 py-2">備註</th>
</tr>
</thead>
<tbody>
<tr
v-for="r in residentsB"
:key="r.bed"
class="border-b last:border-b-0 hover:bg-gray-50"
>
<td class="px-3 py-2 font-mono">{{ r.bed }}</td>
<td class="px-3 py-2">{{ r.name }}</td>
<td class="px-3 py-2">{{ r.gender }}</td>
<td class="px-3 py-2">{{ r.age }}</td>
<td class="px-3 py-2">{{ r.condition }}</td>
<td class="px-3 py-2">{{ r.note }}</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
<!-- body住院資訊 -->
<div v-else-if="modalType === 'inpatients'">
<h4 class="text-lg font-semibold text-gray-700 mb-3">清單</h4>
<div class="overflow-x-auto">
<table class="min-w-full text-sm text-brand-black">
<thead>
<tr class="text-left bg-gray-50">
<th class="px-3 py-2">床位</th>
<th class="px-3 py-2">姓名</th>
<th class="px-3 py-2">醫院與科別</th>
<th class="px-3 py-2">病歷號</th>
<th class="px-3 py-2">狀況</th>
</tr>
</thead>
<tbody>
<tr
v-for="p in inpatients"
:key="p.recordNo"
class="border-b last:border-b-0 hover:bg-gray-50"
>
<td class="px-3 py-2 font-mono">{{ p.bed }}</td>
<td class="px-3 py-2">{{ p.name }}</td>
<td class="px-3 py-2">{{ p.hospitalDept }}</td>
<td class="px-3 py-2 font-mono">{{ p.recordNo }}</td>
<td class="px-3 py-2">{{ p.status }}</td>
</tr>
</tbody>
</table>
<div
class="col-span-5 grid grid-rows-12 justify-start items-start gap-6"
>
<div class="h-[240px] row-span-6">
<img
src="/img/building_photo_headquarter.png"
alt="機構照片"
class="w-full h-full rounded-md object-cover"
/>
</div>
<div class="row-span-4">
<p class="text-brand-gray text-sm">
崇恩長期照顧集團是國內第一家取得ISO認證的長期照顧機構集合了醫師群與資深護理群及照顧服務員們在大南部地區照顧每一位需要我們的長輩
</p>
</div>
</div>
<!-- footer -->
<div class="mt-6 text-right">
<button
class="btn bg-brand-green text-white px-4 py-2 rounded-lg"
@click="closeModal"
>
關閉
</button>
<!-- Progress bars -->
<div class="col-span-6 flex flex-col gap-4">
<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"
/>
</div>
</div>
</div>
<div class="bg-black">
<!-- chart -->
</div>
</section>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
</div>
<!-- 高度比重5 -->
<div class="flex-[5] grid grid-cols-2 gap-2">
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow p-5 flex flex-col justify-center items-start"
>
<h4 class="text-xl text-gray-600 font-bold">近三個月比較</h4>
<div ref="chartARef" class="w-full h-full"></div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow p-5 flex flex-col justify-center items-start"
>
<h4 class="text-xl text-gray-600 font-bold">近六個月比較</h4>
<div ref="chartBRef" class="w-full h-full"></div>
</div>
</div>
<!-- 高度比重5 -->
<div
class="bg-white bg-opacity-70 rounded-xl shadow p-5 flex flex-col justify-center items-start flex-[5]"
>
<h4 class="text-xl text-gray-600 font-bold">與去年同期比較</h4>
<div ref="chartCRef" class="w-full h-full"></div>
</div>
<!-- 素資料 -->
<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>
</section>
<!-- 中間 -->
<section class="bg-white/50 rounded-md shadow p-3"></section>
<!-- 右側 -->
<section class="bg-white/50 rounded-md shadow p-3"></section>
</section>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
import * as echarts from "echarts";
import { brand } from "@/styles/palette";
// ---- DOM Refs ----
const chartARef = ref(null);
const chartBRef = ref(null);
const chartCRef = ref(null);
// ---- Chart instances ----
let chartA, chartB, chartC;
//
function genTrend(len, start = 50, drift = 0.6, noise = 20) {
const arr = [];
let v = start;
for (let i = 0; i < len; i++) {
v = v + drift + (Math.random() * noise * 2 - noise);
arr.push(Number(v.toFixed(1)));
}
return arr;
}
const commonGrid = {
left: 40,
right: 20,
top: 50,
bottom: 30,
containLabel: true,
};
const commonTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
// A
function buildOptionA() {
const x = Array.from({ length: 8 }, (_, i) => `op${i + 1}`);
const legends = ["6月", "7月", "8月"];
const series = legends.map((label, idx) => ({
name: label,
type: "bar",
emphasis: { focus: "series" },
data: genTrend(x.length, 40 + idx * 2, 0.5 + idx * 0.1, 1.5),
barWidth: 5,
}));
return {
legend: { top: 10 },
tooltip: commonTooltip,
grid: commonGrid,
xAxis: { type: "category", data: x },
yAxis: { type: "value" },
//
color: [brand.green, brand.greenLight, brand.yellow],
series,
};
}
// B
function buildOptionB() {
const x = Array.from({ length: 8 }, (_, i) => `op${i + 1}`);
const legends = ["3月", "4月", "5月", "6月", "7月", "8月"];
const series = legends.map((label, idx) => ({
name: label,
type: "bar",
emphasis: { focus: "series" },
data: genTrend(x.length, 35 + idx * 1.5, 0.4 + idx * 0.06, 1.6),
barWidth: 5,
}));
return {
legend: { type: "scroll", top: 10 },
tooltip: commonTooltip,
grid: commonGrid,
xAxis: { type: "category", data: x },
yAxis: { type: "value" },
color: [
brand.green,
brand.greenLight,
brand.yellow,
brand.purpleLight,
brand.purple,
brand.purpleDark,
],
series,
};
}
// C
function buildOptionC() {
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}`);
const lastYear = genTrend(12, 36, 0.8, 2);
const thisYear = lastYear.map(
(v, i) => Number((v + 5 + Math.sin(i / 2) * 3).toFixed(1)) //
);
return {
legend: { top: 10, data: ["去年", "今年"] },
tooltip: { trigger: "axis" },
grid: commonGrid,
xAxis: { type: "category", data: months, boundaryGap: false },
yAxis: { type: "value" },
color: [brand.purple, brand.green],
series: [
{
name: "去年",
type: "line",
smooth: true,
data: lastYear,
},
{ name: "今年", type: "line", smooth: true, data: thisYear },
],
};
}
// & resize
function initCharts() {
if (chartARef.value && !chartA) {
chartA = echarts.init(chartARef.value);
chartA.setOption(buildOptionA());
}
if (chartBRef.value && !chartB) {
chartB = echarts.init(chartBRef.value);
chartB.setOption(buildOptionB());
}
if (chartCRef.value && !chartC) {
chartC = echarts.init(chartCRef.value);
chartC.setOption(buildOptionC());
}
handleResize();
}
let resizeObserver;
function handleResize() {
const resize = () => {
chartA && chartA.resize();
chartB && chartB.resize();
chartC && chartC.resize();
};
window.addEventListener("resize", resize);
const rootEl = document.querySelector("section.flex.flex-col");
if (rootEl) {
resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(rootEl);
}
}
function disposeCharts() {
resizeObserver && resizeObserver.disconnect();
window.removeEventListener("resize", handleResize);
chartA && chartA.dispose();
chartB && chartB.dispose();
chartC && chartC.dispose();
chartA = chartB = chartC = null;
}
onMounted(async () => {
await nextTick();
initCharts();
});
onUnmounted(() => {
disposeCharts();
});
/* ===== Modal 狀態 ===== */
const showModal = ref(false);
const modalType = ref(null);
const modalTitle = computed(() => {
if (modalType.value === "residents") return "住民資訊";
if (modalType.value === "inpatients") return "住院資訊";
return "";
});
function openModal(type) {
modalType.value = type;
showModal.value = true;
nextTick(() => {
const dialog = document.querySelector('[role="dialog"]');
if (dialog) dialog.focus();
});
}
function closeModal() {
showModal.value = false;
modalType.value = null;
}
/* ===== 工具產生不含「4」的床位號 ===== */
function generateBeds(count, start = 1001) {
const arr = [];
let n = start;
while (arr.length < count) {
if (!String(n).includes("4")) arr.push(n);
n++;
}
return arr;
}
/* ===== 假資料:住民 ===== */
const bedsResidents = generateBeds(12, 1001);
const residentsAll = [
{
bed: bedsResidents[0],
name: "王小明",
gender: "男",
age: 78,
condition: "穩定",
note: "喜歡下棋",
area: "A",
},
{
bed: bedsResidents[1],
name: "陳美麗",
gender: "女",
age: 82,
condition: "需輕度協助",
note: "糖尿病控制中",
area: "A",
},
{
bed: bedsResidents[2],
name: "林大志",
gender: "男",
age: 74,
condition: "穩定",
note: "行動慢",
area: "A",
},
{
bed: bedsResidents[3],
name: "張翠華",
gender: "女",
age: 80,
condition: "復健中",
note: "膝關節置換術後",
area: "A",
},
{
bed: bedsResidents[4],
name: "黃國榮",
gender: "男",
age: 85,
condition: "需中度協助",
note: "夜間易醒",
area: "A",
},
{
bed: bedsResidents[5],
name: "李佩珊",
gender: "女",
age: 76,
condition: "穩定",
note: "對花粉過敏",
area: "A",
},
{
bed: bedsResidents[6],
name: "吳大同",
gender: "男",
age: 79,
condition: "穩定",
note: "喜歡園藝",
area: "B",
},
{
bed: bedsResidents[7],
name: "周怡君",
gender: "女",
age: 81,
condition: "需輕度協助",
note: "高血壓",
area: "B",
},
{
bed: bedsResidents[8],
name: "曾文龍",
gender: "男",
age: 77,
condition: "復健中",
note: "髖關節手術後",
area: "B",
},
{
bed: bedsResidents[9],
name: "蔡淑芬",
gender: "女",
age: 83,
condition: "穩定",
note: "喜歡編織",
area: "B",
},
{
bed: bedsResidents[10],
name: "許建宏",
gender: "男",
age: 75,
condition: "需中度協助",
note: "睡眠品質不佳",
area: "B",
},
{
bed: bedsResidents[11],
name: "簡婉婷",
gender: "女",
age: 78,
condition: "穩定",
note: "對海鮮過敏",
area: "B",
},
];
const residentsA = residentsAll.filter((r) => r.area === "A");
const residentsB = residentsAll.filter((r) => r.area === "B");
/* ===== 假資料:住院 ===== */
const bedsInpatient = generateBeds(4, 1020);
const inpatients = [
{
bed: bedsInpatient[0],
name: "劉書豪",
hospitalDept: "台大醫院/心臟內科",
recordNo: "A1023001",
status: "加護中",
},
{
bed: bedsInpatient[1],
name: "高雅筑",
hospitalDept: "榮總/新陳代謝科",
recordNo: "B1023007",
status: "住院觀察",
},
{
bed: bedsInpatient[2],
name: "方志明",
hospitalDept: "長庚/骨科",
recordNo: "C1023011",
status: "術後恢復",
},
{
bed: bedsInpatient[3],
name: "鄭于庭",
hospitalDept: "國泰/神經內科",
recordNo: "D1023020",
status: "檢查中",
},
];
/* ESC 關閉 */
function onKeydown(e) {
if (e.key === "Escape") closeModal();
}
onMounted(() => window.addEventListener("keydown", onKeydown));
onUnmounted(() => window.removeEventListener("keydown", onKeydown));
import ProgressBar from "./ProgressBar.vue";
</script>
<style lang="scss" scoped></style>

View File

@ -1,11 +1,11 @@
<template>
<section
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden"
class="flex flex-col gap-2 h-[calc(100vh-72px-24px)] overflow-hidden"
>
<!-- 高度比重2 -->
<div class="flex-[2] grid grid-cols-4 gap-2">
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">住民人數</p>
@ -29,7 +29,7 @@
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">住院人數</p>
@ -53,7 +53,7 @@
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
@ -77,7 +77,7 @@
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
@ -105,7 +105,7 @@
<!-- 高度比重2 -->
<div class="flex-[2] grid grid-cols-4 gap-2">
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
@ -129,7 +129,7 @@
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
@ -153,7 +153,7 @@
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
@ -177,7 +177,7 @@
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
@ -205,14 +205,14 @@
<!-- 高度比重5 -->
<div class="flex-[5] grid grid-cols-2 gap-2 text-gray-300">
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center"
class="col-span-1 bg-white bg-opacity-70 rounded-md shadow px-4 py-3 flex flex-col justify-center items-center"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">近三個月比較</h4>
<div ref="chartARef" class="w-[90%] h-[90%]"></div> -->
</div>
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center"
class="col-span-1 bg-white bg-opacity-70 rounded-md shadow px-4 py-3 flex flex-col justify-center items-center"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">近六個月比較</h4>
@ -222,7 +222,7 @@
<!-- 高度比重5 -->
<div
class="bg-white bg-opacity-70 text-gray-300 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center flex-[5]"
class="bg-white bg-opacity-70 text-gray-300 rounded-md shadow px-4 py-3 flex flex-col justify-center items-center flex-[5]"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">與去年同期比較</h4>

View File

@ -1,400 +0,0 @@
<template>
<section
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden"
>
<!-- 高度比重2 -->
<div class="flex-[2] grid grid-cols-4 gap-2">
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">住民人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">36/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">住院人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">1/36</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
</div>
<!-- 高度比重2 -->
<div class="flex-[2] grid grid-cols-4 gap-2">
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] font-nats">36/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] font-nats">1/36</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
</div>
<!-- 高度比重5 -->
<div class="flex-[5] grid grid-cols-2 gap-2 text-gray-300">
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">近三個月比較</h4>
<div ref="chartARef" class="w-[90%] h-[90%]"></div> -->
</div>
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">近六個月比較</h4>
<div ref="chartBRef" class="w-[90%] h-[90%]"></div> -->
</div>
</div>
<!-- 高度比重5 -->
<div class="flex-[5] grid grid-cols-2 gap-2 text-gray-300">
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">近三個月比較</h4>
<div ref="chartARef" class="w-[90%] h-[90%]"></div> -->
</div>
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">近六個月比較</h4>
<div ref="chartBRef" class="w-[90%] h-[90%]"></div> -->
</div>
</div>
</section>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from "vue";
import * as echarts from "echarts";
import { brand } from "@/styles/palette";
// ---- DOM Refs ----
const chartARef = ref(null);
const chartBRef = ref(null);
const chartCRef = ref(null);
// ---- Chart instances ----
let chartA, chartB, chartC;
//
function genTrend(len, start = 50, drift = 0.6, noise = 20) {
const arr = [];
let v = start;
for (let i = 0; i < len; i++) {
v = v + drift + (Math.random() * noise * 2 - noise);
arr.push(Number(v.toFixed(1)));
}
return arr;
}
const commonGrid = {
left: 40,
right: 20,
top: 50,
bottom: 30,
containLabel: true,
};
const commonTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
// A
function buildOptionA() {
const x = Array.from({ length: 8 }, (_, i) => `${i + 1}`);
const legends = ["6月", "7月", "8月"];
const series = legends.map((label, idx) => ({
name: label,
type: "bar",
emphasis: { focus: "series" },
data: genTrend(x.length, 40 + idx * 2, 0.5 + idx * 0.1, 1.5),
}));
return {
legend: { top: 10 },
tooltip: commonTooltip,
grid: commonGrid,
xAxis: { type: "category", data: x },
yAxis: { type: "value" },
//
color: [brand.green, brand.greenLight, brand.yellow],
series,
};
}
// B
function buildOptionB() {
const x = Array.from({ length: 8 }, (_, i) => `${i + 1}`);
const legends = ["3月", "4月", "5月", "6月", "7月", "8月"];
const series = legends.map((label, idx) => ({
name: label,
type: "bar",
emphasis: { focus: "series" },
data: genTrend(x.length, 35 + idx * 1.5, 0.4 + idx * 0.06, 1.6),
}));
return {
legend: { type: "scroll", top: 10 },
tooltip: commonTooltip,
grid: commonGrid,
xAxis: { type: "category", data: x },
yAxis: { type: "value" },
color: [
brand.green,
brand.greenLight,
brand.yellow,
brand.purpleLight,
brand.purple,
brand.gray,
],
series,
};
}
// C
function buildOptionC() {
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}`);
const lastYear = genTrend(12, 48, 0.3, 1.8);
const thisYear = lastYear.map((v, i) =>
Number((v + 1 + Math.sin(i / 2)).toFixed(1))
);
return {
legend: { top: 10, data: ["去年", "今年"] },
tooltip: { trigger: "axis" },
grid: commonGrid,
xAxis: { type: "category", data: months, boundaryGap: false },
yAxis: { type: "value" },
color: [brand.green, brand.purple],
series: [
{ name: "去年", type: "line", smooth: true, data: lastYear },
{ name: "今年", type: "line", smooth: true, data: thisYear },
],
};
}
// & resize
function initCharts() {
if (chartARef.value && !chartA) {
chartA = echarts.init(chartARef.value);
chartA.setOption(buildOptionA());
}
if (chartBRef.value && !chartB) {
chartB = echarts.init(chartBRef.value);
chartB.setOption(buildOptionB());
}
if (chartCRef.value && !chartC) {
chartC = echarts.init(chartCRef.value);
chartC.setOption(buildOptionC());
}
handleResize();
}
let resizeObserver;
function handleResize() {
const resize = () => {
chartA && chartA.resize();
chartB && chartB.resize();
chartC && chartC.resize();
};
window.addEventListener("resize", resize);
const rootEl = document.querySelector("section.flex.flex-col");
if (rootEl) {
resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(rootEl);
}
}
function disposeCharts() {
resizeObserver && resizeObserver.disconnect();
window.removeEventListener("resize", handleResize);
chartA && chartA.dispose();
chartB && chartB.dispose();
chartC && chartC.dispose();
chartA = chartB = chartC = null;
}
onMounted(async () => {
await nextTick();
initCharts();
});
onUnmounted(() => {
disposeCharts();
});
</script>

View File

@ -6,7 +6,7 @@
<div class="flex-[2] grid grid-cols-4 gap-2">
<!-- 住民人數 Card -->
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
@click="openModal('residents')"
>
<div class="flex justify-center items-center gap-4">
@ -33,7 +33,7 @@
<!-- 住院人數 Card -->
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
@click="openModal('inpatients')"
>
<div class="flex justify-center items-center gap-4">
@ -189,7 +189,7 @@
<!-- footer -->
<div class="mt-6 text-right">
<button
class="btn bg-brand-green text-white px-4 py-2 rounded-lg"
class="btn bg-brand-green text-white px-4 py-2 rounded-md "
@click="closeModal"
>
關閉
@ -199,7 +199,7 @@
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
@ -223,7 +223,7 @@
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-md shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
@ -251,13 +251,13 @@
<!-- 高度比重5 -->
<div class="flex-[5] grid grid-cols-2 gap-2">
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow p-5 flex flex-col justify-center items-start"
class="col-span-1 bg-white bg-opacity-70 rounded-md shadow p-5 flex flex-col justify-center items-start"
>
<h4 class="text-xl text-gray-600 font-bold">近三個月比較</h4>
<div ref="chartARef" class="w-full h-full"></div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow p-5 flex flex-col justify-center items-start"
class="col-span-1 bg-white bg-opacity-70 rounded-md shadow p-5 flex flex-col justify-center items-start"
>
<h4 class="text-xl text-gray-600 font-bold">近六個月比較</h4>
<div ref="chartBRef" class="w-full h-full"></div>
@ -266,7 +266,7 @@
<!-- 高度比重5 -->
<div
class="bg-white bg-opacity-70 rounded-xl shadow p-5 flex flex-col justify-center items-start flex-[5]"
class="bg-white bg-opacity-70 rounded-md shadow p-5 flex flex-col justify-center items-start flex-[5]"
>
<h4 class="text-xl text-gray-600 font-bold">與去年同期比較</h4>
<div ref="chartCRef" class="w-full h-full"></div>

View File

@ -1,382 +0,0 @@
<template>
<section
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden"
>
<!-- 高度比重2 -->
<div class="flex-[2] grid grid-cols-4 gap-2">
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">住民人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">36/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">住院人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">1/36</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] xl:text-[40px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
</div>
<!-- 高度比重2 -->
<div class="flex-[2] grid grid-cols-4 gap-2">
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] font-nats">36/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] font-nats">1/36</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
<div
class="col-span-1 bg-white bg-opacity-70 text-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
>
<div class="flex justify-center items-center gap-4">
<p class="text-sm">其他人數</p>
<span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
/>
</svg>
</span>
</div>
<div class="flex justify-center items-baseline gap-1">
<p class="text-[36px] font-nats">0/49</p>
<p class="text-[12px]"></p>
</div>
</div>
</div>
<!-- 高度比重5 -->
<div class="flex-[5] grid grid-cols-2 gap-2 text-gray-300">
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">近三個月比較</h4>
<div ref="chartARef" class="w-[90%] h-[90%]"></div> -->
</div>
<div
class="col-span-1 bg-white bg-opacity-70 rounded-xl shadow px-4 py-3 flex flex-col justify-center items-center"
>
<p>暫無內容</p>
<!-- <h4 class="text-xl text-gray-600 font-bold">近六個月比較</h4>
<div ref="chartBRef" class="w-[90%] h-[90%]"></div> -->
</div>
</div>
</section>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from "vue";
import * as echarts from "echarts";
import { brand } from "@/styles/palette";
// ---- DOM Refs ----
const chartARef = ref(null);
const chartBRef = ref(null);
const chartCRef = ref(null);
// ---- Chart instances ----
let chartA, chartB, chartC;
//
function genTrend(len, start = 50, drift = 0.6, noise = 20) {
const arr = [];
let v = start;
for (let i = 0; i < len; i++) {
v = v + drift + (Math.random() * noise * 2 - noise);
arr.push(Number(v.toFixed(1)));
}
return arr;
}
const commonGrid = {
left: 40,
right: 20,
top: 50,
bottom: 30,
containLabel: true,
};
const commonTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
// A
function buildOptionA() {
const x = Array.from({ length: 8 }, (_, i) => `${i + 1}`);
const legends = ["6月", "7月", "8月"];
const series = legends.map((label, idx) => ({
name: label,
type: "bar",
emphasis: { focus: "series" },
data: genTrend(x.length, 40 + idx * 2, 0.5 + idx * 0.1, 1.5),
}));
return {
legend: { top: 10 },
tooltip: commonTooltip,
grid: commonGrid,
xAxis: { type: "category", data: x },
yAxis: { type: "value" },
//
color: [brand.green, brand.greenLight, brand.yellow],
series,
};
}
// B
function buildOptionB() {
const x = Array.from({ length: 8 }, (_, i) => `${i + 1}`);
const legends = ["3月", "4月", "5月", "6月", "7月", "8月"];
const series = legends.map((label, idx) => ({
name: label,
type: "bar",
emphasis: { focus: "series" },
data: genTrend(x.length, 35 + idx * 1.5, 0.4 + idx * 0.06, 1.6),
}));
return {
legend: { type: "scroll", top: 10 },
tooltip: commonTooltip,
grid: commonGrid,
xAxis: { type: "category", data: x },
yAxis: { type: "value" },
color: [
brand.green,
brand.greenLight,
brand.yellow,
brand.purpleLight,
brand.purple,
brand.gray,
],
series,
};
}
// C
function buildOptionC() {
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}`);
const lastYear = genTrend(12, 48, 0.3, 1.8);
const thisYear = lastYear.map((v, i) =>
Number((v + 1 + Math.sin(i / 2)).toFixed(1))
);
return {
legend: { top: 10, data: ["去年", "今年"] },
tooltip: { trigger: "axis" },
grid: commonGrid,
xAxis: { type: "category", data: months, boundaryGap: false },
yAxis: { type: "value" },
color: [brand.green, brand.purple],
series: [
{ name: "去年", type: "line", smooth: true, data: lastYear },
{ name: "今年", type: "line", smooth: true, data: thisYear },
],
};
}
// & resize
function initCharts() {
if (chartARef.value && !chartA) {
chartA = echarts.init(chartARef.value);
chartA.setOption(buildOptionA());
}
if (chartBRef.value && !chartB) {
chartB = echarts.init(chartBRef.value);
chartB.setOption(buildOptionB());
}
if (chartCRef.value && !chartC) {
chartC = echarts.init(chartCRef.value);
chartC.setOption(buildOptionC());
}
handleResize();
}
let resizeObserver;
function handleResize() {
const resize = () => {
chartA && chartA.resize();
chartB && chartB.resize();
chartC && chartC.resize();
};
window.addEventListener("resize", resize);
const rootEl = document.querySelector("section.flex.flex-col");
if (rootEl) {
resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(rootEl);
}
}
function disposeCharts() {
resizeObserver && resizeObserver.disconnect();
window.removeEventListener("resize", handleResize);
chartA && chartA.dispose();
chartB && chartB.dispose();
chartC && chartC.dispose();
chartA = chartB = chartC = null;
}
onMounted(async () => {
await nextTick();
initCharts();
});
onUnmounted(() => {
disposeCharts();
});
</script>

View File

@ -1,21 +1,58 @@
import { createRouter, createWebHistory } from "vue-router";
// 匯入各 page
import HomePage from "@/pages/home/index.vue";
import OperationPage from "@/pages/operation/index.vue";
import NursingPage from "@/pages/nursing/index.vue";
import SocialWorkerPage from "@/pages/socialWorker/index.vue";
import NutritionPage from "@/pages/nutrition/index.vue";
// Lazy-loaded pages
const HomePage = () => import("@/pages/home/index.vue");
const OperationPage = () => import("@/pages/operation/index.vue");
const NursingPage = () => import("@/pages/nursing/index.vue");
const routes = [
{ path: "/", name: "Home", component: HomePage },
{ path: "/operation", name: "Operation", component: OperationPage },
{ path: "/nursing", name: "Nursing", component: NursingPage },
{ path: "/social-worker", name: "SocialWorker", component: SocialWorkerPage },
{ path: "/nutrition", name: "Nutrition", component: NutritionPage },
// 首頁 → 使用 HeadquarterLayout無 Forge
{
path: "/",
name: "Home",
component: HomePage,
meta: {
layout: "headquarter",
title: "Home",
keepAlive: true, // 可選:若要保留捲動/表單狀態
},
},
// 其他頁 → 預設使用 MapLayout有地圖
{
path: "/operation",
name: "Operation",
component: OperationPage,
meta: { layout: "map", title: "Operation" },
},
{
path: "/nursing",
name: "Nursing",
component: NursingPage,
meta: { layout: "map", title: "Nursing" },
},
// 404
// {
// path: "/:pathMatch(.*)*",
// name: "NotFound",
// component: () => import("@/pages/_errors/NotFound.vue"),
// meta: { layout: "headquarter", title: "404 Not Found" },
// },
];
export default createRouter({
history: createWebHistory(),
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
// 回上一頁可保留位置,否則回到頂端
scrollBehavior(to, from, saved) {
return saved || { top: 0 };
},
});
router.afterEach((to) => {
const base = "UARK";
document.title = to.meta?.title ? `${to.meta.title}${base}` : base;
});
export default router;

View File

@ -132,7 +132,7 @@
/* ========== 卡片/區塊:品牌一致性 ========== */
@layer components {
.card-brand {
@apply rounded-xl shadow-sm border border-gray-100 bg-white;
@apply rounded-md shadow-sm border border-gray-100 bg-white;
}
.chip-brand {
@apply inline-flex items-center gap-2 rounded-full px-3 py-1 text-sm bg-brand-green-light text-brand-black;
@ -146,7 +146,14 @@
}
}
/* 設定全域字體 */
html,
body,
#app {
height: 100%;
}
body {
margin: 0;
width: 100%; /* 別用 100vw */
overflow-x: hidden; /* 防止偶發橫向卷軸 */
font-family: "Noto Sans TC", sans-serif;
}

View File

@ -9,5 +9,6 @@ export const brand = {
purpleDark: "#7089CA",
yellow: "#E1F391",
black: "#424242",
neutral: "#F0F0F0",
};
gray: "#828282",
grayLight: "#F0F0F0",
};

View File

@ -52,8 +52,10 @@ module.exports = {
dark: "#C4E920",
},
black: "#424242",
neutral: {
DEFAULT: "#F0F0F0",
gray: {
light: "#F0F0F0",
deepLight:"#D2D2D2",
DEFAULT: "#828282",
},
},
},
@ -67,7 +69,7 @@ module.exports = {
primary: "#34D5C8",
secondary: "#C4FBE5",
accent: "#A5BEFF",
neutral: "#424242",
neutral: "#828282",
"base-100": "#ffffff",
warning: "FF8678",
},