feat: 新增 progress bar 切換圖表功能
This commit is contained in:
parent
458cfc7eb0
commit
fbf694ccae
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav
|
<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 + 機構切換 -->
|
<!-- 左側:Logo + 機構切換 -->
|
||||||
<div
|
<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">
|
<RouterLink to="/" class="h-9 md:h-11">
|
||||||
<img src="/img/logo.png" alt="Logo" class="h-full w-auto" />
|
<img src="/img/logo.png" alt="Logo" class="h-full w-auto" />
|
||||||
@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
<!-- 中間:導覽(手機隱藏;平板以上顯示;桌機放大間距) -->
|
<!-- 中間:導覽(手機隱藏;平板以上顯示;桌機放大間距) -->
|
||||||
<div
|
<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 }">
|
<RouterLink to="/" v-slot="{ href, navigate, isExactActive }">
|
||||||
<a
|
<a
|
||||||
@ -102,7 +102,7 @@
|
|||||||
</div>
|
</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
|
<button
|
||||||
class="btn bg-white text-brand-black hover:opacity-90 shadow-md border-none rounded-full p-2"
|
class="btn bg-white text-brand-black hover:opacity-90 shadow-md border-none rounded-full p-2"
|
||||||
aria-label="通知"
|
aria-label="通知"
|
||||||
@ -156,7 +156,7 @@
|
|||||||
|
|
||||||
<!-- 手機:漢堡按鈕-->
|
<!-- 手機:漢堡按鈕-->
|
||||||
<button
|
<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"
|
@click="toggleMobileMenu"
|
||||||
:aria-expanded="isMobileMenuOpen"
|
:aria-expanded="isMobileMenuOpen"
|
||||||
aria-controls="mobile-menu"
|
aria-controls="mobile-menu"
|
||||||
@ -187,8 +187,8 @@
|
|||||||
|
|
||||||
<!-- 手機:全螢幕 ham menu(白底,hover 灰,active 綠) -->
|
<!-- 手機:全螢幕 ham menu(白底,hover 灰,active 綠) -->
|
||||||
<div
|
<div
|
||||||
id="mobile-menu"
|
id="mobile-menu-overlay"
|
||||||
class="md:hidden fixed inset-0 z-[1100] bg-white p-4 flex flex-col gap-4 transition-opacity duration-200"
|
class="sm:hidden fixed inset-0 z-[1100] bg-white p-4 flex flex-col gap-4 transition-opacity duration-200"
|
||||||
:class="
|
:class="
|
||||||
isMobileMenuOpen
|
isMobileMenuOpen
|
||||||
? 'opacity-100 pointer-events-auto'
|
? 'opacity-100 pointer-events-auto'
|
||||||
@ -357,6 +357,146 @@
|
|||||||
<div class="mt-auto text-xs text-gray-500">© U-ARK</div>
|
<div class="mt-auto text-xs text-gray-500">© U-ARK</div>
|
||||||
</div>
|
</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>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -440,12 +580,12 @@ const handleKeydown = (e) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener("click", onClickOutside);
|
document.addEventListener("click", onClickOutside);
|
||||||
document.addEventListener("keydown", handleKeydown); // ✅ 只綁一次
|
document.addEventListener("keydown", handleKeydown);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener("click", onClickOutside);
|
document.removeEventListener("click", onClickOutside);
|
||||||
document.removeEventListener("keydown", handleKeydown); // ✅ 只解一次
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-2">
|
<div
|
||||||
<!-- 標題列(可選 icon 插槽) -->
|
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">
|
<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" />
|
<slot name="icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 進度條(文字覆蓋在進度條上,預設靠左) -->
|
<!-- 進度條 -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<progress
|
<progress
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@ -21,10 +31,24 @@
|
|||||||
></progress>
|
></progress>
|
||||||
|
|
||||||
<span
|
<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"
|
:class="textPosClass"
|
||||||
>
|
>
|
||||||
{{ currentLocale }} / {{ totalLocale }}
|
{{ 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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -32,18 +56,22 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
defineOptions({ inheritAttrs: false });
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: { type: String, required: true },
|
label: { type: String, required: true },
|
||||||
current: { type: Number, required: true },
|
current: { type: Number, required: true },
|
||||||
total: { 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(() => {
|
const textPosClass = computed(() => {
|
||||||
switch (props.textAlign) {
|
switch (props.textAlign) {
|
||||||
case "center":
|
case "center":
|
||||||
@ -57,4 +85,13 @@ const textPosClass = computed(() => {
|
|||||||
|
|
||||||
const currentLocale = computed(() => props.current.toLocaleString());
|
const currentLocale = computed(() => props.current.toLocaleString());
|
||||||
const totalLocale = computed(() => props.total.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>
|
</script>
|
||||||
|
@ -1,59 +1,102 @@
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<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">
|
<section
|
||||||
<!-- 機構照片 + 機構說明 + 核心資料 bars + chart -->
|
class="grid grid-rows-12 gap-2 order-2 lg:order-1 min-h-0 lg:h-full"
|
||||||
|
>
|
||||||
|
<!-- 上半:照片/說明 + 進度條(獨立 section) -->
|
||||||
<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
|
<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
|
<div class="h-[200px] row-span-6">
|
||||||
class="w-full col-span-1 grid grid-rows-12 justify-start items-start gap-6"
|
<img
|
||||||
>
|
src="/img/building/headquarter.png"
|
||||||
<div class="h-[240px] row-span-6">
|
alt="機構照片"
|
||||||
<img
|
class="w-full h-full rounded-sm object-cover"
|
||||||
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>
|
</div>
|
||||||
<!-- Progress bars -->
|
<div class="row-span-4">
|
||||||
<div class="w-full col-span-1 flex flex-col gap-6">
|
<p
|
||||||
<ProgressBar
|
class="text-brand-gray bg-white/50 text-sm border rounded-sm px-2 py-3 border-brand-gray-light"
|
||||||
label="現在住民/全立案床數"
|
>
|
||||||
:current="240"
|
崇恩長期照顧集團是國內第一家取得ISO認證的長期照顧機構,集合了醫師群與資深護理群及照顧服務員們,在大南部地區照顧每一位需要我們的長輩。
|
||||||
:total="360"
|
</p>
|
||||||
/>
|
|
||||||
<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>
|
||||||
<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 ref="chartEl" class="w-full h-full"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -131,13 +174,19 @@
|
|||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<!-- 中間 -->
|
<!-- 中間 -->
|
||||||
<section class="bg-white/50 rounded-md shadow p-3">
|
<section class="bg-white/50 rounded-md shadow p-3 order-1 lg:order-2">
|
||||||
<div ref="mapEl" class="w-full h-full rounded-md overflow-hidden"></div>
|
<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>
|
||||||
<!-- 右側 -->
|
<!-- 右側 -->
|
||||||
<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>
|
<h3 class="text-xl font-bold">今日活動</h3>
|
||||||
<!-- 可捲動內容區(水平) -->
|
<!-- 可捲動內容區(水平) -->
|
||||||
<div class="flex flex-col gap-4 mb-6">
|
<div class="flex flex-col gap-4 mb-6">
|
||||||
@ -204,7 +253,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<h3 class="text-xl font-bold">今日異常事件</h3>
|
||||||
<!-- 可捲動內容區(水平) -->
|
<!-- 可捲動內容區(水平) -->
|
||||||
<div class="flex flex-col gap-4 mb-6">
|
<div class="flex flex-col gap-4 mb-6">
|
||||||
@ -216,7 +267,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="w-[100px]">時間</th>
|
<th class="w-[100px]">時間</th>
|
||||||
<th class="w-[160px]">機構</th>
|
<th class="w-[160px]">機構</th>
|
||||||
<th class="w-[180px]">事件</th>
|
<th class="w-[100px]">事件</th>
|
||||||
|
|
||||||
<th class="w-[120px] text-center">查看詳情</th>
|
<th class="w-[120px] text-center">查看詳情</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -275,7 +326,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<h3 class="text-xl font-bold">今日派車總表</h3>
|
||||||
|
|
||||||
<!-- 可捲動內容區(水平) -->
|
<!-- 可捲動內容區(水平) -->
|
||||||
@ -358,6 +411,8 @@ import L from "leaflet";
|
|||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { brand } from "@/styles/palette";
|
import { brand } from "@/styles/palette";
|
||||||
|
|
||||||
|
const activeKey = ref("residents");
|
||||||
|
|
||||||
// 產生最近 7 天的標籤(M/D)
|
// 產生最近 7 天的標籤(M/D)
|
||||||
function last7DaysLabels() {
|
function last7DaysLabels() {
|
||||||
const labels = [];
|
const labels = [];
|
||||||
@ -370,13 +425,104 @@ function last7DaysLabels() {
|
|||||||
return labels;
|
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);
|
const chartEl = ref(null);
|
||||||
let chart;
|
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(() => {
|
onMounted(() => {
|
||||||
if (!chartEl.value) return;
|
if (!chartEl.value) return;
|
||||||
chart = echarts.init(chartEl.value);
|
chart = echarts.init(chartEl.value);
|
||||||
@ -384,67 +530,23 @@ onMounted(() => {
|
|||||||
const labels = last7DaysLabels();
|
const labels = last7DaysLabels();
|
||||||
|
|
||||||
chart.setOption({
|
chart.setOption({
|
||||||
color: [brand.green, brand.purple], // 藍、綠(可換你的品牌色)
|
grid: { left: 36, right: 16, top: 44, bottom: 56, containLabel: true },
|
||||||
tooltip: {
|
tooltip: { trigger: "axis", axisPointer: { type: "line" }, confine: true },
|
||||||
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: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
data: labels,
|
data: last7DaysLabels(),
|
||||||
axisLine: { lineStyle: { color: brand.gray } },
|
axisLine: { lineStyle: { color: brand.gray } },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
axisLabel: { color: brand.gray },
|
axisLabel: { color: brand.gray },
|
||||||
splitLine: { show: false },
|
splitLine: { show: false },
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: { type: "value", name: "數量" },
|
||||||
type: "value",
|
series: [],
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateChartByKey("residents");
|
||||||
|
|
||||||
// RWD
|
// RWD
|
||||||
const onResize = () => chart?.resize();
|
const onResize = () => chart?.resize();
|
||||||
window.addEventListener("resize", onResize);
|
window.addEventListener("resize", onResize);
|
||||||
@ -608,9 +710,25 @@ onMounted(() => {
|
|||||||
paddingBottomRight: [24, 24],
|
paddingBottomRight: [24, 24],
|
||||||
maxZoom: 15,
|
maxZoom: 15,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化後延遲重算一次尺寸(避免初次渲染壓扁)
|
||||||
|
setTimeout(() => map?.invalidateSize(), 0);
|
||||||
|
|
||||||
|
// 視窗尺寸改變時也重算(與 ECharts 相同)
|
||||||
|
const onResize = () => {
|
||||||
|
chart?.resize?.();
|
||||||
|
map?.invalidateSize?.();
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
|
||||||
|
// 存起來以便卸載時移除
|
||||||
|
map.__onResize = onResize;
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
if (map?.__onResize) window.removeEventListener("resize", map.__onResize);
|
||||||
|
if (chart?.__onResize) window.removeEventListener("resize", chart.__onResize);
|
||||||
|
|
||||||
if (map) {
|
if (map) {
|
||||||
map.remove();
|
map.remove();
|
||||||
map = null;
|
map = null;
|
||||||
|
@ -9,6 +9,7 @@ export const brand = {
|
|||||||
purpleLight: "#D5E1FF",
|
purpleLight: "#D5E1FF",
|
||||||
purpleDark: "#7089CA",
|
purpleDark: "#7089CA",
|
||||||
yellow: "#E1F391",
|
yellow: "#E1F391",
|
||||||
|
yellowDark: "#C4E920",
|
||||||
black: "#424242",
|
black: "#424242",
|
||||||
gray: "#828282",
|
gray: "#828282",
|
||||||
grayLight: "#E9E9E9",
|
grayLight: "#E9E9E9",
|
||||||
|
Loading…
Reference in New Issue
Block a user