feat: 新增 progress bar 切換圖表功能
This commit is contained in:
parent
458cfc7eb0
commit
fbf694ccae
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<nav
|
||||
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]"
|
||||
class="sticky top-0 w-full h-14 lg: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 items-center gap-4 sm:gap-8 shrink-0 w-[240px] md:w-[300px] lg:w-[340px]"
|
||||
class="flex items-center gap-4 sm:gap-8 shrink-0 w-[240px] md:w-[280px] lg:w-[340px]"
|
||||
>
|
||||
<RouterLink to="/" class="h-9 md:h-11">
|
||||
<img src="/img/logo.png" alt="Logo" class="h-full w-auto" />
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
<!-- 中間:導覽(手機隱藏;平板以上顯示;桌機放大間距) -->
|
||||
<div
|
||||
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]"
|
||||
class="hidden lg:grid min-w-[300px] grid-cols-3 items-center bg-white shadow-md rounded-full md:px-0 xl:min-w-[360px]"
|
||||
>
|
||||
<RouterLink to="/" v-slot="{ href, navigate, isExactActive }">
|
||||
<a
|
||||
@ -102,7 +102,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 右側:通知 + 使用者(手機只顯示圖示;平板顯示文字) -->
|
||||
<div class="hidden md:flex items-center gap-3 sm:gap-6">
|
||||
<div class="hidden lg: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="通知"
|
||||
@ -156,7 +156,7 @@
|
||||
|
||||
<!-- 手機:漢堡按鈕-->
|
||||
<button
|
||||
class="md:hidden btn bg-white text-brand-black hover:opacity-90 shadow-md border-none rounded-md p-2"
|
||||
class="lg: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"
|
||||
@ -187,8 +187,8 @@
|
||||
|
||||
<!-- 手機:全螢幕 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"
|
||||
id="mobile-menu-overlay"
|
||||
class="sm: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'
|
||||
@ -357,6 +357,146 @@
|
||||
<div class="mt-auto text-xs text-gray-500">© U-ARK</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平板直式 modal:在 640px~1023px 顯示,位置在漢堡鈕下方 -->
|
||||
<div
|
||||
v-show="isMobileMenuOpen"
|
||||
class="hidden sm:block lg:hidden absolute right-4 top-16 z-[1100] w-[88vw] sm:w-[240px] bg-white rounded-xl shadow-xl border border-gray-100 p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="主選單(平板)"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 頂部:標題 + 關閉 -->
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<p class="font-semibold text-brand-black text-base">選單</p>
|
||||
</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 my-3" />
|
||||
|
||||
<!-- 通知 + 使用者(放在全螢幕 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()"
|
||||
>
|
||||
<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>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@ -440,12 +580,12 @@ const handleKeydown = (e) => {
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", onClickOutside);
|
||||
document.addEventListener("keydown", handleKeydown); // ✅ 只綁一次
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", onClickOutside);
|
||||
document.removeEventListener("keydown", handleKeydown); // ✅ 只解一次
|
||||
document.removeEventListener("keydown", handleKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -1,12 +1,22 @@
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<!-- 標題列(可選 icon 插槽) -->
|
||||
<div
|
||||
class="space-y-2 cursor-pointer group outline-none"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="handleSelect"
|
||||
@keydown.enter.prevent="handleSelect"
|
||||
@keydown.space.prevent="handleSelect"
|
||||
>
|
||||
<!-- 標題列 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="block font-medium">{{ label }}</label>
|
||||
<label
|
||||
class="block font-medium transition-colors duration-200 group-hover:text-brand-purple-dark cursor-pointer"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
|
||||
<!-- 進度條(文字覆蓋在進度條上,預設靠左) -->
|
||||
<!-- 進度條 -->
|
||||
<div class="relative">
|
||||
<progress
|
||||
v-bind="$attrs"
|
||||
@ -21,10 +31,24 @@
|
||||
></progress>
|
||||
|
||||
<span
|
||||
class="pointer-events-none absolute bottom-0 flex items-center text-[20px] font-nats text-brand-black/80"
|
||||
class="w-full absolute bottom-0 flex justify-between items-center text-[20px] font-nats pe-5 text-brand-black/80 group-hover:text-brand-purple-dark"
|
||||
:class="textPosClass"
|
||||
>
|
||||
{{ currentLocale }} / {{ totalLocale }}
|
||||
<span aria-hidden="true">
|
||||
<svg
|
||||
class="text-brand-gray/50"
|
||||
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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -32,18 +56,22 @@
|
||||
|
||||
<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
|
||||
textAlign: { type: String, default: "left" },
|
||||
chartKey: { type: String, required: true },
|
||||
currentLegend: { type: String, required: true },
|
||||
totalLegend: { type: String, required: true },
|
||||
height: { type: [Number, String], default: 5 },
|
||||
});
|
||||
|
||||
const heightClass = computed(() => `h-${props.height}`);
|
||||
const emit = defineEmits(["select"]);
|
||||
|
||||
const heightClass = computed(() => `h-${props.height}`);
|
||||
const textPosClass = computed(() => {
|
||||
switch (props.textAlign) {
|
||||
case "center":
|
||||
@ -57,4 +85,13 @@ const textPosClass = computed(() => {
|
||||
|
||||
const currentLocale = computed(() => props.current.toLocaleString());
|
||||
const totalLocale = computed(() => props.total.toLocaleString());
|
||||
|
||||
function handleSelect() {
|
||||
// 把要顯示在圖表頂部的標題也一併送出
|
||||
emit("select", {
|
||||
key: props.chartKey,
|
||||
legends: [props.currentLegend, props.totalLegend],
|
||||
titleText: `progress ${currentLocale.value} / ${totalLocale.value}`,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
@ -1,59 +1,102 @@
|
||||
<template>
|
||||
<section
|
||||
class="grid grid-cols-3 gap-2 h-[calc(100vh-72px-32px)] justify-center"
|
||||
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"
|
||||
>
|
||||
<!-- 左側 -->
|
||||
<section class="grid grid-rows-12 gap-2">
|
||||
<!-- 機構照片 + 機構說明 + 核心資料 bars + chart -->
|
||||
<section
|
||||
class="grid grid-rows-12 gap-2 order-2 lg:order-1 min-h-0 lg:h-full"
|
||||
>
|
||||
<!-- 上半:照片/說明 + 進度條(獨立 section) -->
|
||||
<section
|
||||
class="h-[740px] row-span-9 bg-white/50 rounded-md shadow py-6 px-4 flex flex-col items-start gap-4"
|
||||
class="row-span-5 bg-white/50 rounded-md shadow py-6 px-4 grid grid-cols-1 md:grid-cols-2 items-start gap-6 min-h-0"
|
||||
>
|
||||
<!-- 照片 + 說明(手機隱藏、平板/桌機顯示) -->
|
||||
<div
|
||||
class="w-full h-[400px] grid grid-cols-2 justify-center items-start gap-6 px-2"
|
||||
class="w-full grid grid-rows-12 justify-start items-start gap-6 hidden sm:grid"
|
||||
>
|
||||
<div
|
||||
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/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 class="h-[200px] row-span-6">
|
||||
<img
|
||||
src="/img/building/headquarter.png"
|
||||
alt="機構照片"
|
||||
class="w-full h-full rounded-sm object-cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- Progress bars -->
|
||||
<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" />
|
||||
<div class="row-span-4">
|
||||
<p
|
||||
class="text-brand-gray bg-white/50 text-sm border rounded-sm px-2 py-3 border-brand-gray-light"
|
||||
>
|
||||
崇恩長期照顧集團是國內第一家取得ISO認證的長期照顧機構,集合了醫師群與資深護理群及照顧服務員們,在大南部地區照顧每一位需要我們的長輩。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-[240px] flex justify-center items-center">
|
||||
<!-- chart -->
|
||||
|
||||
<!-- Progress bars -->
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
<!-- Residents -->
|
||||
<ProgressBar
|
||||
label="現在住民/全立案床數"
|
||||
:current="240"
|
||||
:total="360"
|
||||
chartKey="residents"
|
||||
currentLegend="現在住民"
|
||||
totalLegend="全立案床數"
|
||||
@select="({ key }) => updateChartByKey(key)"
|
||||
/>
|
||||
|
||||
<!-- Vacancy -->
|
||||
<ProgressBar
|
||||
label="目前空床數/全立案床數"
|
||||
:current="120"
|
||||
:total="360"
|
||||
chartKey="vacancy"
|
||||
currentLegend="目前空床數"
|
||||
totalLegend="全立案床數"
|
||||
@select="({ key }) => updateChartByKey(key)"
|
||||
/>
|
||||
|
||||
<!-- Hospitalized -->
|
||||
<ProgressBar
|
||||
label="今日住院/當月累積住院"
|
||||
:current="8"
|
||||
:total="50"
|
||||
chartKey="hospitalized"
|
||||
currentLegend="今日住院"
|
||||
totalLegend="當月累積住院"
|
||||
@select="({ key }) => updateChartByKey(key)"
|
||||
/>
|
||||
|
||||
<!-- Move-in -->
|
||||
<ProgressBar
|
||||
label="今日新人入住/當月累積入住"
|
||||
:current="12"
|
||||
:total="50"
|
||||
chartKey="movein"
|
||||
currentLegend="今日入住"
|
||||
totalLegend="當月累積入住"
|
||||
@select="({ key }) => updateChartByKey(key)"
|
||||
/>
|
||||
|
||||
<!-- Move-out -->
|
||||
<ProgressBar
|
||||
label="今日離院/累積離院"
|
||||
:current="8"
|
||||
:total="50"
|
||||
chartKey="moveout"
|
||||
currentLegend="今日離院"
|
||||
totalLegend="累積離院"
|
||||
@select="({ key }) => updateChartByKey(key)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 下半:圖表(獨立 section,有內距) -->
|
||||
<section
|
||||
class="row-span-4 bg-white/50 rounded-md shadow min-h-0 flex items-stretch"
|
||||
>
|
||||
<!-- 這層專門控制圖表外圍留白 -->
|
||||
<div
|
||||
class="w-full h-full p-3 sm:p-4 md:p-6 min-h-[200px] sm:min-h-[240px] md:min-h-[300px]"
|
||||
>
|
||||
<div ref="chartEl" class="w-full h-full"></div>
|
||||
</div>
|
||||
</section>
|
||||
@ -131,13 +174,19 @@
|
||||
</section>
|
||||
</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 class="bg-white/50 rounded-md shadow p-3 order-1 lg:order-2">
|
||||
<div
|
||||
ref="mapEl"
|
||||
class="w-full rounded-md overflow-hidden min-h-[240px] sm:min-h-[300px] md:h-[420px] lg:h-full"
|
||||
style="aspect-ratio: 4 / 3"
|
||||
></div>
|
||||
</section>
|
||||
<!-- 右側 -->
|
||||
<section class="bg-white/50 rounded-md shadow p-3 flex flex-col gap-2">
|
||||
<section class="flex flex-col gap-2 order-4 lg:order-3">
|
||||
<!-- 表格 今日活動 -->
|
||||
<section class="rounded-md shadow p-3 flex flex-col min-h-0 gap-3">
|
||||
<section
|
||||
class="bg-white/50 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">
|
||||
@ -204,7 +253,9 @@
|
||||
</div>
|
||||
</section>
|
||||
<!-- 表格 今日異常事件 -->
|
||||
<section class="rounded-md shadow p-3 flex flex-col min-h-0 gap-3">
|
||||
<section
|
||||
class="bg-white/50 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">
|
||||
@ -216,7 +267,7 @@
|
||||
<tr>
|
||||
<th class="w-[100px]">時間</th>
|
||||
<th class="w-[160px]">機構</th>
|
||||
<th class="w-[180px]">事件</th>
|
||||
<th class="w-[100px]">事件</th>
|
||||
|
||||
<th class="w-[120px] text-center">查看詳情</th>
|
||||
</tr>
|
||||
@ -275,7 +326,9 @@
|
||||
</div>
|
||||
</section>
|
||||
<!-- 表格 今日派車總表 -->
|
||||
<section class="rounded-md shadow p-3 flex flex-col min-h-0 gap-3">
|
||||
<section
|
||||
class="bg-white/50 rounded-md shadow p-3 flex flex-col min-h-0 gap-3"
|
||||
>
|
||||
<h3 class="text-xl font-bold">今日派車總表</h3>
|
||||
|
||||
<!-- 可捲動內容區(水平) -->
|
||||
@ -358,6 +411,8 @@ import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { brand } from "@/styles/palette";
|
||||
|
||||
const activeKey = ref("residents");
|
||||
|
||||
// 產生最近 7 天的標籤(M/D)
|
||||
function last7DaysLabels() {
|
||||
const labels = [];
|
||||
@ -370,13 +425,104 @@ function last7DaysLabels() {
|
||||
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;
|
||||
|
||||
// 圖表資料(依你的實際資料調整)
|
||||
const chartDataMap = {
|
||||
residents: {
|
||||
legends: ["現在住民", "全立案床數"],
|
||||
a: [15, 58, 25, 75, 90, 38, 76],
|
||||
b: [26, 78, 85, 32, 30, 52, 72],
|
||||
},
|
||||
vacancy: {
|
||||
legends: ["目前空床數", "全立案床數"],
|
||||
a: [30, 22, 18, 40, 55, 28, 33],
|
||||
b: Array(7).fill(60),
|
||||
},
|
||||
hospitalized: {
|
||||
legends: ["今日住院", "當月累積住院"],
|
||||
a: [2, 6, 3, 9, 8, 4, 5],
|
||||
b: [5, 12, 18, 22, 28, 36, 42],
|
||||
},
|
||||
movein: {
|
||||
legends: ["今日入住", "當月累積入住"],
|
||||
a: [1, 3, 2, 4, 5, 3, 6],
|
||||
b: [4, 8, 12, 17, 22, 30, 36],
|
||||
},
|
||||
moveout: {
|
||||
legends: ["今日離院", "累積離院"],
|
||||
a: [1, 2, 1, 3, 2, 1, 2],
|
||||
b: [5, 7, 9, 12, 14, 15, 17],
|
||||
},
|
||||
};
|
||||
|
||||
// ProgressBar 畫面上的 current/total(用來組圖表標題)
|
||||
const progressTitleMap = {
|
||||
residents: { current: 240, total: 360 },
|
||||
vacancy: { current: 120, total: 360 },
|
||||
hospitalized: { current: 8, total: 50 },
|
||||
movein: { current: 12, total: 50 },
|
||||
moveout: { current: 8, total: 50 },
|
||||
};
|
||||
|
||||
function updateChartByKey(key) {
|
||||
activeKey.value = key;
|
||||
const conf = chartDataMap[key];
|
||||
|
||||
chart.setOption({
|
||||
color: [brand.green, brand.purple],
|
||||
title: {
|
||||
// 顯示 legend 名稱:現在住民/全立案床數
|
||||
text: `${conf.legends[0]}/${conf.legends[1]}`,
|
||||
left: "center",
|
||||
top: 0,
|
||||
textStyle: {
|
||||
color: brand.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
fontFamily: '"Noto Sans TC"',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: conf.legends,
|
||||
bottom: 8,
|
||||
icon: "circle",
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 24,
|
||||
textStyle: { color: brand.gray },
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "數量",
|
||||
nameGap: 12,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: brand.gray },
|
||||
splitLine: { show: true, lineStyle: { color: brand.grayLight } },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: conf.legends[0],
|
||||
type: "line",
|
||||
data: conf.a,
|
||||
symbol: "circle",
|
||||
symbolSize: 6,
|
||||
lineStyle: { width: 2 },
|
||||
},
|
||||
{
|
||||
name: conf.legends[1],
|
||||
type: "line",
|
||||
data: conf.b,
|
||||
symbol: "circle",
|
||||
symbolSize: 6,
|
||||
lineStyle: { width: 2 },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!chartEl.value) return;
|
||||
chart = echarts.init(chartEl.value);
|
||||
@ -384,67 +530,23 @@ onMounted(() => {
|
||||
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 },
|
||||
grid: { left: 36, right: 16, top: 44, bottom: 56, containLabel: true },
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "line" }, confine: true },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: labels,
|
||||
data: last7DaysLabels(),
|
||||
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,
|
||||
yAxis: { type: "value", name: "數量" },
|
||||
series: [],
|
||||
});
|
||||
|
||||
updateChartByKey("residents");
|
||||
|
||||
// RWD
|
||||
const onResize = () => chart?.resize();
|
||||
window.addEventListener("resize", onResize);
|
||||
@ -608,9 +710,25 @@ onMounted(() => {
|
||||
paddingBottomRight: [24, 24],
|
||||
maxZoom: 15,
|
||||
});
|
||||
|
||||
// 初始化後延遲重算一次尺寸(避免初次渲染壓扁)
|
||||
setTimeout(() => map?.invalidateSize(), 0);
|
||||
|
||||
// 視窗尺寸改變時也重算(與 ECharts 相同)
|
||||
const onResize = () => {
|
||||
chart?.resize?.();
|
||||
map?.invalidateSize?.();
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
// 存起來以便卸載時移除
|
||||
map.__onResize = onResize;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (map?.__onResize) window.removeEventListener("resize", map.__onResize);
|
||||
if (chart?.__onResize) window.removeEventListener("resize", chart.__onResize);
|
||||
|
||||
if (map) {
|
||||
map.remove();
|
||||
map = null;
|
||||
|
@ -9,6 +9,7 @@ export const brand = {
|
||||
purpleLight: "#D5E1FF",
|
||||
purpleDark: "#7089CA",
|
||||
yellow: "#E1F391",
|
||||
yellowDark: "#C4E920",
|
||||
black: "#424242",
|
||||
gray: "#828282",
|
||||
grayLight: "#E9E9E9",
|
||||
|
Loading…
Reference in New Issue
Block a user