fix: 修正營運頁圖表為箱型圖
This commit is contained in:
parent
fbf694ccae
commit
ccfef49c47
@ -1,3 +1,314 @@
|
||||
<template>
|
||||
<div class="relative w-full h-full min-h-full">
|
||||
<div id="forge-preview" ref="forgeDom" class="absolute inset-0"></div>
|
||||
<!-- Popovers:永遠顯示在每顆 sprite 上方(跟著相機更新) -->
|
||||
<div class="absolute inset-0 z-20 pointer-events-none opacity-90">
|
||||
<div
|
||||
v-for="L in filteredLabels"
|
||||
:key="L.id"
|
||||
class="absolute -translate-x-1/2 -translate-y-full cursor-pointer"
|
||||
:style="{
|
||||
left: L.x + 'px',
|
||||
top: L.y - 10 + 'px',
|
||||
zIndex: activeLabelId === L.id ? 40 : 30,
|
||||
}"
|
||||
@click="bringToFrontById(L.id)"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent.stop="openResidentModal(L, $event)"
|
||||
@keydown.space.prevent.stop="openResidentModal(L, $event)"
|
||||
aria-label="查看 {{ L.data.name }} 詳細資訊"
|
||||
>
|
||||
<!-- 箭頭(下方置中,指向 sprite) -->
|
||||
<!-- 外層(邊框色) -->
|
||||
<span
|
||||
class="absolute left-1/2 -bottom-2 -translate-x-1/2 w-0 h-0 border-x-8 border-x-transparent border-t-8 border-t-white"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<ul class="list-none">
|
||||
<!-- 第一行:床號永遠顯示 -->
|
||||
<li class="flex justify-between items-center gap-1">
|
||||
<div>
|
||||
<span
|
||||
class="inline-block w-2 h-2 rounded-full mr-1 align-middle"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
L.data.state === 'offnormal' ? BRAND_RED : BRAND_GREEN,
|
||||
}"
|
||||
></span>
|
||||
|
||||
<!-- 住院中才顯示黃色 icon -->
|
||||
<span
|
||||
v-if="L.data.special"
|
||||
class="w-2 h-2 text-brand-yellow-dark inline-block align-middle me-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- 床號:永遠顯示 -->
|
||||
<span class="align-middle">{{ L.data.name }}</span>
|
||||
</div>
|
||||
<!-- 眼睛 icon -->
|
||||
<div class="text-gray-400" title="查看詳細">
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
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>
|
||||
</li>
|
||||
|
||||
<!-- 第二行:住民基本資料 -->
|
||||
<li>
|
||||
{{
|
||||
isVacantWithoutSpecial(L)
|
||||
? "-"
|
||||
: `${L.data.residentsName}|${L.data.residentsSex}|${L.data.residentsAge}`
|
||||
}}
|
||||
</li>
|
||||
|
||||
<!-- 第三行:入住日期 -->
|
||||
<li>
|
||||
{{
|
||||
isVacantWithoutSpecial(L)
|
||||
? "-"
|
||||
: `入住日期:${L.data.startTime}`
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 樓層切換 -->
|
||||
<div
|
||||
class="absolute top-4 left-4 text-sm flex justify-center items-center gap-2 z-10 bg-white border rounded-md shadow"
|
||||
>
|
||||
<p class="ps-4 py-2">樓層|</p>
|
||||
|
||||
<button
|
||||
class="px-4 py-2 rounded-md"
|
||||
:class="activeFloor === '9F' ? 'bg-brand-green-light' : 'bg-white'"
|
||||
@click="onClickFloor('9F')"
|
||||
aria-pressed="activeFloor==='9F'"
|
||||
>
|
||||
1F
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="activeFloor === '8F' ? 'bg-brand-green-light' : 'bg-white'"
|
||||
@click="onClickFloor('8F')"
|
||||
aria-pressed="activeFloor==='8F'"
|
||||
>
|
||||
2F
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 床位資訊-->
|
||||
<div
|
||||
class="absolute top-16 left-4 text-sm flex justify-center items-center z-10 bg-white border rounded-md shadow ps-4"
|
||||
>
|
||||
<p class="py-2">床位資訊|</p>
|
||||
|
||||
<div class="flex justify-start items-center">
|
||||
<!-- 無顯示 -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'none' ? 'bg-brand-green-light bg-opacity-50' : ''
|
||||
"
|
||||
@click="selectInfo({ label: '無顯示', value: 'none' })"
|
||||
:aria-pressed="selectedInfo === 'none'"
|
||||
>
|
||||
<span>不顯示</span>
|
||||
</button>
|
||||
|
||||
<!-- 有住民(綠) -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'occupied'
|
||||
? 'bg-brand-green-light bg-opacity-50'
|
||||
: ''
|
||||
"
|
||||
@click="selectInfo({ label: '有住民', value: 'occupied' })"
|
||||
:aria-pressed="selectedInfo === 'occupied'"
|
||||
>
|
||||
<span class="w-2 h-2 bg-brand-green rounded-full inline-block"></span>
|
||||
<span>有住民</span>
|
||||
</button>
|
||||
|
||||
<!-- 空床(紅) -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'vacant'
|
||||
? 'bg-brand-green-light bg-opacity-50'
|
||||
: ''
|
||||
"
|
||||
@click="selectInfo({ label: '空床', value: 'vacant' })"
|
||||
:aria-pressed="selectedInfo === 'vacant'"
|
||||
>
|
||||
<span class="w-2 h-2 bg-brand-red rounded-full inline-block"></span>
|
||||
<span>空床</span>
|
||||
</button>
|
||||
|
||||
<!-- 住院中(紅 + 黃色警示) -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'hospitalized'
|
||||
? 'bg-brand-green-light bg-opacity-50'
|
||||
: ''
|
||||
"
|
||||
@click="selectInfo({ label: '住院中', value: 'hospitalized' })"
|
||||
:aria-pressed="selectedInfo === 'hospitalized'"
|
||||
>
|
||||
<span class="w-2 h-2 text-brand-yellow-dark inline-block">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>住院中</span>
|
||||
</button>
|
||||
|
||||
<!-- 請假中(灰) -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'leave' ? 'bg-brand-green-light bg-opacity-50' : ''
|
||||
"
|
||||
@click="selectInfo({ label: '請假中', value: 'leave' })"
|
||||
:aria-pressed="selectedInfo === 'leave'"
|
||||
>
|
||||
<span class="w-2 h-2 text-gray-400 inline-block">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>請假中</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分區切換(多選|預設不選=顯示全部) -->
|
||||
<div
|
||||
class="absolute bottom-4 left-4 text-sm flex justify-center items-center gap-2 z-10 bg-white border rounded-md shadow"
|
||||
>
|
||||
<p class="ps-4 py-2">查看分區|</p>
|
||||
|
||||
<button
|
||||
v-for="zone in zones"
|
||||
:key="zone"
|
||||
class="px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
isZoneSelected(zone)
|
||||
? 'bg-brand-green-light bg-opacity-50'
|
||||
: 'bg-white'
|
||||
"
|
||||
@click="toggleZone(zone)"
|
||||
:aria-pressed="isZoneSelected(zone)"
|
||||
>
|
||||
{{ zone }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中央 Modal:病患資訊 -->
|
||||
<div
|
||||
v-if="modalOpen"
|
||||
class="fixed inset-0 z-[120] flex items-center justify-center"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- 背景遮罩 -->
|
||||
<div class="absolute inset-0 bg-black/40" @click="closeResidentModal"></div>
|
||||
|
||||
<!-- Modal 內容 -->
|
||||
<div
|
||||
class="relative z-[130] w-[min(92vw,500px)] max-h-[84vh] overflow-auto bg-white rounded-2xl shadow-xl p-6"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 標題 -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold tracking-widest">病患資訊</h3>
|
||||
<button
|
||||
class="px-3 py-1 rounded-md hover:bg-gray-100"
|
||||
@click="closeResidentModal"
|
||||
aria-label="關閉"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 內容清單 -->
|
||||
<ul class="list-none text-sm space-y-2">
|
||||
<li><span class="text-gray-500">姓名:</span>{{ modalData?.name }}</li>
|
||||
<li><span class="text-gray-500">性別:</span>{{ modalData?.sex }}</li>
|
||||
<li><span class="text-gray-500">年齡:</span>{{ modalData?.age }}</li>
|
||||
<li>
|
||||
<span class="text-gray-500">入住日期:</span
|
||||
>{{ modalData?.startTime }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-500">身體狀況:</span
|
||||
>{{ modalData?.healthStatus }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-500">服藥狀況:</span
|
||||
>{{ modalData?.medicationStatus }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-500">特殊事件:</span
|
||||
>{{ modalData?.specialEvent }}
|
||||
</li>
|
||||
<li class="whitespace-pre-wrap break-words">
|
||||
<span class="text-gray-500">備註:</span>{{ modalData?.note }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
@ -389,7 +700,7 @@ function rebuildLabelsAfterNextRender() {
|
||||
}
|
||||
|
||||
// ====== 分區切換(多選|預設不選=顯示全部) ======
|
||||
const zones = ["VIP", "換管", "失能", "失智"];
|
||||
const zones = ["一般", "氧氣"];
|
||||
const selectedZones = ref(new Set());
|
||||
const toggleZone = (zone) => {
|
||||
const set = new Set(selectedZones.value);
|
||||
@ -569,318 +880,6 @@ onUnmounted(() => {
|
||||
document.removeEventListener("keydown", onKeydownInfo);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full min-h-full">
|
||||
<div id="forge-preview" ref="forgeDom" class="absolute inset-0"></div>
|
||||
<!-- Popovers:永遠顯示在每顆 sprite 上方(跟著相機更新) -->
|
||||
<div class="absolute inset-0 z-20 pointer-events-none opacity-90">
|
||||
<div
|
||||
v-for="L in filteredLabels"
|
||||
:key="L.id"
|
||||
class="absolute -translate-x-1/2 -translate-y-full cursor-pointer"
|
||||
:style="{
|
||||
left: L.x + 'px',
|
||||
top: L.y - 10 + 'px',
|
||||
zIndex: activeLabelId === L.id ? 40 : 30,
|
||||
}"
|
||||
@click="bringToFrontById(L.id)"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
tabindex="0"
|
||||
@keydown.enter.prevent.stop="openResidentModal(L, $event)"
|
||||
@keydown.space.prevent.stop="openResidentModal(L, $event)"
|
||||
aria-label="查看 {{ L.data.name }} 詳細資訊"
|
||||
>
|
||||
<!-- 箭頭(下方置中,指向 sprite) -->
|
||||
<!-- 外層(邊框色) -->
|
||||
<span
|
||||
class="absolute left-1/2 -bottom-2 -translate-x-1/2 w-0 h-0 border-x-8 border-x-transparent border-t-8 border-t-white"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<ul class="list-none">
|
||||
<!-- 第一行:床號永遠顯示 -->
|
||||
<li class="flex justify-between items-center gap-1">
|
||||
<div>
|
||||
<span
|
||||
class="inline-block w-2 h-2 rounded-full mr-1 align-middle"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
L.data.state === 'offnormal' ? BRAND_RED : BRAND_GREEN,
|
||||
}"
|
||||
></span>
|
||||
|
||||
<!-- 住院中才顯示黃色 icon -->
|
||||
<span
|
||||
v-if="L.data.special"
|
||||
class="w-2 h-2 text-brand-yellow-dark inline-block align-middle me-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<!-- 床號:永遠顯示 -->
|
||||
<span class="align-middle">{{ L.data.name }}</span>
|
||||
</div>
|
||||
<!-- 眼睛 icon -->
|
||||
<div class="text-gray-400" title="查看詳細">
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
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>
|
||||
</li>
|
||||
|
||||
<!-- 第二行:住民基本資料 -->
|
||||
<li>
|
||||
{{
|
||||
isVacantWithoutSpecial(L)
|
||||
? "-"
|
||||
: `${L.data.residentsName}|${L.data.residentsSex}|${L.data.residentsAge}`
|
||||
}}
|
||||
</li>
|
||||
|
||||
<!-- 第三行:入住日期 -->
|
||||
<li>
|
||||
{{
|
||||
isVacantWithoutSpecial(L)
|
||||
? "-"
|
||||
: `入住日期:${L.data.startTime}`
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 樓層切換 -->
|
||||
<div
|
||||
class="absolute top-4 left-4 text-sm flex justify-center items-center gap-2 z-10 bg-white border rounded-md shadow"
|
||||
>
|
||||
<p class="ps-4 py-2">樓層|</p>
|
||||
|
||||
<button
|
||||
class="px-4 py-2 rounded-md"
|
||||
:class="activeFloor === '9F' ? 'bg-brand-green-light' : 'bg-white'"
|
||||
@click="onClickFloor('9F')"
|
||||
aria-pressed="activeFloor==='9F'"
|
||||
>
|
||||
1F
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="activeFloor === '8F' ? 'bg-brand-green-light' : 'bg-white'"
|
||||
@click="onClickFloor('8F')"
|
||||
aria-pressed="activeFloor==='8F'"
|
||||
>
|
||||
2F
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 分區切換(多選|預設不選=顯示全部) -->
|
||||
<div
|
||||
class="absolute top-16 left-4 text-sm flex justify-center items-center gap-2 z-10 bg-white border rounded-md shadow"
|
||||
>
|
||||
<p class="ps-4 py-2">分區|</p>
|
||||
|
||||
<button
|
||||
v-for="zone in zones"
|
||||
:key="zone"
|
||||
class="px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
isZoneSelected(zone)
|
||||
? 'bg-brand-green-light bg-opacity-50'
|
||||
: 'bg-white'
|
||||
"
|
||||
@click="toggleZone(zone)"
|
||||
:aria-pressed="isZoneSelected(zone)"
|
||||
>
|
||||
{{ zone }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 床位資訊-->
|
||||
<div
|
||||
class="absolute bottom-4 left-4 text-sm flex justify-center items-center z-10 bg-white border rounded-md shadow ps-4"
|
||||
>
|
||||
<p class="py-2">床位資訊|</p>
|
||||
|
||||
<div class="flex justify-start items-center">
|
||||
<!-- 無顯示 -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'none' ? 'bg-brand-green-light bg-opacity-50' : ''
|
||||
"
|
||||
@click="selectInfo({ label: '無顯示', value: 'none' })"
|
||||
:aria-pressed="selectedInfo === 'none'"
|
||||
>
|
||||
<span>不顯示</span>
|
||||
</button>
|
||||
|
||||
<!-- 有住民(綠) -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'occupied'
|
||||
? 'bg-brand-green-light bg-opacity-50'
|
||||
: ''
|
||||
"
|
||||
@click="selectInfo({ label: '有住民', value: 'occupied' })"
|
||||
:aria-pressed="selectedInfo === 'occupied'"
|
||||
>
|
||||
<span class="w-2 h-2 bg-brand-green rounded-full inline-block"></span>
|
||||
<span>有住民</span>
|
||||
</button>
|
||||
|
||||
<!-- 空床(紅) -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'vacant'
|
||||
? 'bg-brand-green-light bg-opacity-50'
|
||||
: ''
|
||||
"
|
||||
@click="selectInfo({ label: '空床', value: 'vacant' })"
|
||||
:aria-pressed="selectedInfo === 'vacant'"
|
||||
>
|
||||
<span class="w-2 h-2 bg-brand-red rounded-full inline-block"></span>
|
||||
<span>空床</span>
|
||||
</button>
|
||||
|
||||
<!-- 住院中(紅 + 黃色警示) -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'hospitalized'
|
||||
? 'bg-brand-green-light bg-opacity-50'
|
||||
: ''
|
||||
"
|
||||
@click="selectInfo({ label: '住院中', value: 'hospitalized' })"
|
||||
:aria-pressed="selectedInfo === 'hospitalized'"
|
||||
>
|
||||
<span class="w-2 h-2 text-brand-yellow-dark inline-block">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>住院中</span>
|
||||
</button>
|
||||
|
||||
<!-- 請假中(灰) -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-gray-100"
|
||||
:class="
|
||||
selectedInfo === 'leave' ? 'bg-brand-green-light bg-opacity-50' : ''
|
||||
"
|
||||
@click="selectInfo({ label: '請假中', value: 'leave' })"
|
||||
:aria-pressed="selectedInfo === 'leave'"
|
||||
>
|
||||
<span class="w-2 h-2 text-gray-400 inline-block">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>請假中</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中央 Modal:病患資訊 -->
|
||||
<div
|
||||
v-if="modalOpen"
|
||||
class="fixed inset-0 z-[120] flex items-center justify-center"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<!-- 背景遮罩 -->
|
||||
<div class="absolute inset-0 bg-black/40" @click="closeResidentModal"></div>
|
||||
|
||||
<!-- Modal 內容 -->
|
||||
<div
|
||||
class="relative z-[130] w-[min(92vw,500px)] max-h-[84vh] overflow-auto bg-white rounded-2xl shadow-xl p-6"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 標題 -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold tracking-widest">病患資訊</h3>
|
||||
<button
|
||||
class="px-3 py-1 rounded-md hover:bg-gray-100"
|
||||
@click="closeResidentModal"
|
||||
aria-label="關閉"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 內容清單 -->
|
||||
<ul class="list-none text-sm space-y-2">
|
||||
<li><span class="text-gray-500">姓名:</span>{{ modalData?.name }}</li>
|
||||
<li><span class="text-gray-500">性別:</span>{{ modalData?.sex }}</li>
|
||||
<li><span class="text-gray-500">年齡:</span>{{ modalData?.age }}</li>
|
||||
<li>
|
||||
<span class="text-gray-500">入住日期:</span
|
||||
>{{ modalData?.startTime }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-500">身體狀況:</span
|
||||
>{{ modalData?.healthStatus }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-500">服藥狀況:</span
|
||||
>{{ modalData?.medicationStatus }}
|
||||
</li>
|
||||
<li>
|
||||
<span class="text-gray-500">特殊事件:</span
|
||||
>{{ modalData?.specialEvent }}
|
||||
</li>
|
||||
<li class="whitespace-pre-wrap break-words">
|
||||
<span class="text-gray-500">備註:</span>{{ modalData?.note }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.adsk-viewing-viewer {
|
||||
background-color: transparent !important;
|
||||
|
0
src/hooks/usePagination.js
Normal file
0
src/hooks/usePagination.js
Normal file
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<nav
|
||||
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]"
|
||||
class="sticky top-0 w-full h-14 lg:h-16 bg-white/50 bg-opacity-80 shadow-md flex justify-between items-center px-4 sm:px-6 lg:px-8 z-[80]"
|
||||
>
|
||||
<!-- 左側:Logo + 機構切換 -->
|
||||
<div
|
||||
@ -10,7 +10,7 @@
|
||||
<img src="/img/logo.png" alt="Logo" class="h-full w-auto" />
|
||||
</RouterLink>
|
||||
|
||||
<div class="relative">
|
||||
<div class="relative" v-if="!isHome">
|
||||
<!-- 手機到桌機都顯示機構名稱,維持桌機尺寸 -->
|
||||
<div
|
||||
ref="triggerRef"
|
||||
@ -18,13 +18,13 @@
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-haspopup="true"
|
||||
:aria-expanded="isOpen"
|
||||
@click="toggle"
|
||||
@keydown.enter.prevent="toggle"
|
||||
@keydown.space.prevent="toggle"
|
||||
:aria-expanded="isFacilityOpen"
|
||||
@click="toggleFacility"
|
||||
@keydown.enter.prevent="toggleFacility"
|
||||
@keydown.space.prevent="toggleFacility"
|
||||
>
|
||||
<!-- 直接顯示,不再隱藏 -->
|
||||
<span>{{ displayLabel }}</span>
|
||||
<span class="tracking-wider">{{ displayLabel }}</span>
|
||||
<!-- caret icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -38,9 +38,9 @@
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown(小螢幕靠左;桌機可改靠右) -->
|
||||
<!-- Dropdown-->
|
||||
<div
|
||||
v-show="isOpen"
|
||||
v-show="isFacilityOpen"
|
||||
ref="panelRef"
|
||||
class="absolute top-12 left-0 md:top-12 md:left-0 lg:left-0 z-50 w-56 md:w-64 rounded-md border border-gray-100 bg-white shadow-lg p-2"
|
||||
@click.stop
|
||||
@ -50,7 +50,7 @@
|
||||
v-for="item in facilities"
|
||||
:key="item"
|
||||
@click="selectItem(item)"
|
||||
class="px-3 py-2 rounded-md cursor-pointer hover:bg-gray-100"
|
||||
class="px-3 py-2 rounded-md cursor-pointer hover:bg-gray-100 tracking-wider"
|
||||
:class="selectedItem === item ? 'bg-brand-green-light' : ''"
|
||||
>
|
||||
{{ item }}
|
||||
@ -508,28 +508,41 @@ import {
|
||||
onUnmounted,
|
||||
defineOptions,
|
||||
nextTick,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
// ====== 首頁隱藏(機構選單功能)======
|
||||
const route = useRoute();
|
||||
const isHome = computed(() => route.name === "home" || route.path === "/");
|
||||
// Dropdown 狀態
|
||||
const isOpen = ref(false);
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
(p) => {
|
||||
if (p === "/" || route.name === "home") isOpen.value = false;
|
||||
}
|
||||
);
|
||||
|
||||
// ====== Dropdown(機構清單)======
|
||||
const isOpen = ref(false);
|
||||
const isFacilityOpen = ref(false);
|
||||
const triggerRef = ref(null);
|
||||
const panelRef = ref(null);
|
||||
|
||||
const facilities = [
|
||||
"總部",
|
||||
"護祐護理之家",
|
||||
"崇祐護理之家",
|
||||
"崇祐長照中心(養護型)",
|
||||
"育祐護理之家",
|
||||
"崇恩長照中心(養護型)",
|
||||
"崇恩護理之家",
|
||||
"崇祐護理之家",
|
||||
"護祐護理之家",
|
||||
"育祐護理之家",
|
||||
"崇智護理之家",
|
||||
"崇恩居家護理所",
|
||||
"傳心日間照顧中心",
|
||||
"敬慈居家服務中心",
|
||||
"崇恩長照中心(養護型)",
|
||||
"崇祐長照中心(養護型)",
|
||||
"傳祐長照中心(養護型)",
|
||||
"中安崇恩長照中心(養護型)",
|
||||
"崇智護理之家",
|
||||
"慈祐長照中心(養護型)",
|
||||
"中安崇恩長照中心(養護型)",
|
||||
];
|
||||
const selectedItem = ref(facilities[0]);
|
||||
|
||||
@ -543,8 +556,8 @@ const displayLabel = computed(() => {
|
||||
});
|
||||
defineOptions({ name: "NavBar" });
|
||||
|
||||
const toggle = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
const toggleFacility = () => {
|
||||
isFacilityOpen.value = !isFacilityOpen.value;
|
||||
};
|
||||
|
||||
const onClickOutside = (e) => {
|
||||
@ -553,7 +566,7 @@ const onClickOutside = (e) => {
|
||||
const clickedInsideTrigger = triggerRef.value.contains(t);
|
||||
const clickedInsidePanel = panelRef.value.contains(t);
|
||||
if (!clickedInsideTrigger && !clickedInsidePanel) {
|
||||
isOpen.value = false;
|
||||
isFacilityOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -573,8 +586,8 @@ const closeMobileMenu = () => {
|
||||
// ====== 全域鍵盤(Esc 同時關閉兩種面板)======
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key === "Escape") {
|
||||
isOpen.value = false; // 關 dropdown
|
||||
isMobileMenuOpen.value = false; // 關手機抽屜
|
||||
isFacilityOpen.value = false; // 關 dropdown
|
||||
isMobileMenuOpen.value = false; // 關 ham menu
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
<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="progress w-full h-5 bg-brand-gray-lighter 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"
|
||||
|
@ -8,7 +8,7 @@
|
||||
>
|
||||
<!-- 上半:照片/說明 + 進度條(獨立 section) -->
|
||||
<section
|
||||
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"
|
||||
class="row-span-5 bg-white/70 rounded-md shadow p-6 grid grid-cols-1 md:grid-cols-2 items-start gap-6 min-h-0"
|
||||
>
|
||||
<!-- 照片 + 說明(手機隱藏、平板/桌機顯示) -->
|
||||
<div
|
||||
@ -21,9 +21,9 @@
|
||||
class="w-full h-full rounded-sm object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="row-span-4">
|
||||
<div class="row-span-6">
|
||||
<p
|
||||
class="text-brand-gray bg-white/50 text-sm border rounded-sm px-2 py-3 border-brand-gray-light"
|
||||
class="text-brand-gray bg-white/70 text-sm border rounded-sm px-2 py-3 border-brand-gray-light"
|
||||
>
|
||||
崇恩長期照顧集團是國內第一家取得ISO認證的長期照顧機構,集合了醫師群與資深護理群及照顧服務員們,在大南部地區照顧每一位需要我們的長輩。
|
||||
</p>
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
<!-- Move-in -->
|
||||
<ProgressBar
|
||||
label="今日新人入住/當月累積入住"
|
||||
label="今日新入住/當月累積入住"
|
||||
:current="12"
|
||||
:total="50"
|
||||
chartKey="movein"
|
||||
@ -78,12 +78,12 @@
|
||||
|
||||
<!-- Move-out -->
|
||||
<ProgressBar
|
||||
label="今日離院/累積離院"
|
||||
label="今日離院/當月累積離院"
|
||||
:current="8"
|
||||
:total="50"
|
||||
chartKey="moveout"
|
||||
currentLegend="今日離院"
|
||||
totalLegend="累積離院"
|
||||
totalLegend="當月累積離院"
|
||||
@select="({ key }) => updateChartByKey(key)"
|
||||
/>
|
||||
</div>
|
||||
@ -91,7 +91,7 @@
|
||||
|
||||
<!-- 下半:圖表(獨立 section,有內距) -->
|
||||
<section
|
||||
class="row-span-4 bg-white/50 rounded-md shadow min-h-0 flex items-stretch"
|
||||
class="row-span-4 bg-white/70 rounded-md shadow min-h-0 flex items-stretch"
|
||||
>
|
||||
<!-- 這層專門控制圖表外圍留白 -->
|
||||
<div
|
||||
@ -103,7 +103,7 @@
|
||||
|
||||
<!-- 葷/素資料 -->
|
||||
<section class="row-span-3 grid grid-cols-2 gap-2">
|
||||
<div class="col-span-1 bg-white/50 rounded-md shadow p-6">
|
||||
<div class="col-span-1 bg-white/70 rounded-md shadow p-6">
|
||||
<div
|
||||
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-start items-center gap-3 p-3"
|
||||
>
|
||||
@ -137,7 +137,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-1 bg-white/50 rounded-md shadow p-6">
|
||||
<div class="col-span-1 bg-white/70 rounded-md shadow p-6">
|
||||
<div
|
||||
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-start items-center gap-3 p-3"
|
||||
>
|
||||
@ -173,52 +173,45 @@
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- 中間 -->
|
||||
<section class="bg-white/50 rounded-md shadow p-3 order-1 lg:order-2">
|
||||
<section class="bg-white/70 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="flex flex-col gap-2 order-4 lg:order-3">
|
||||
<!-- 表格 今日活動 -->
|
||||
<section
|
||||
class="bg-white/50 rounded-md shadow p-3 flex flex-col min-h-0 gap-3"
|
||||
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3"
|
||||
>
|
||||
<h3 class="text-xl font-bold">今日活動</h3>
|
||||
<h3 class="text-2xl font-bold">今日活動</h3>
|
||||
<!-- 可捲動內容區(水平) -->
|
||||
<div class="flex flex-col gap-4 mb-6">
|
||||
<div class="w-full overflow-x-auto overflow-y-auto">
|
||||
<table class="table whitespace-nowrap">
|
||||
<thead
|
||||
class="bg-brand-gray-light text-brand-black sticky top-0 z-[1]"
|
||||
class="bg-brand-gray-lighter text-brand-black sticky top-0 z-[1]"
|
||||
>
|
||||
<tr>
|
||||
<th class="w-[120px]">時間</th>
|
||||
<th class="w-[160px]">機構</th>
|
||||
<th class="w-[160px]">活動名稱</th>
|
||||
<th class="w-[120px] text-center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in pagedRows"
|
||||
:key="row.id"
|
||||
class="transition-colors duration-150 hover:bg-brand-gray-light focus-visible:bg-gray-100/70"
|
||||
class="transition-colors duration-150 hover:bg-brand-gray-lighter focus-visible:bg-gray-100/70"
|
||||
>
|
||||
<td>{{ row.time }}</td>
|
||||
<td>{{ row.org }}</td>
|
||||
<td class="truncate">{{ row.title }}</td>
|
||||
<td class="text-center">
|
||||
<button
|
||||
class="btn btn-link btn-xs px-0 text-brand-purple-dark !no-underline hover:!no-underline hover:opacity-80 focus-visible:!no-underline"
|
||||
@click="viewDetail(row)"
|
||||
>
|
||||
查看詳情
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -226,12 +219,12 @@
|
||||
</div>
|
||||
<!-- 分頁 -->
|
||||
<div class="mt-3 flex items-center justify-between px-3">
|
||||
<span class="text-sm text-gray-500">
|
||||
<span class="text-sm text-brand-gray">
|
||||
共 {{ total }} 筆,每頁 {{ pageSize }} 筆
|
||||
</span>
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple disabled:!opacity-100 disabled:!text-gray-300 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
>
|
||||
@ -243,7 +236,7 @@
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple disabled:!opacity-100 disabled:!text-gray-300 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="currentPage++"
|
||||
>
|
||||
@ -254,22 +247,20 @@
|
||||
</section>
|
||||
<!-- 表格 今日異常事件 -->
|
||||
<section
|
||||
class="bg-white/50 rounded-md shadow p-3 flex flex-col min-h-0 gap-3"
|
||||
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3"
|
||||
>
|
||||
<h3 class="text-xl font-bold">今日異常事件</h3>
|
||||
<h3 class="text-2xl font-bold">今日異常事件</h3>
|
||||
<!-- 可捲動內容區(水平) -->
|
||||
<div class="flex flex-col gap-4 mb-6">
|
||||
<div class="w-full overflow-x-auto overflow-y-auto">
|
||||
<table class="table whitespace-nowrap">
|
||||
<thead
|
||||
class="bg-brand-gray-light text-brand-black sticky top-0 z-[1]"
|
||||
class="bg-brand-gray-lighter text-brand-black sticky top-0 z-[1]"
|
||||
>
|
||||
<tr>
|
||||
<th class="w-[100px]">時間</th>
|
||||
<th class="w-[160px]">機構</th>
|
||||
<th class="w-[100px]">事件</th>
|
||||
|
||||
<th class="w-[120px] text-center">查看詳情</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -277,20 +268,11 @@
|
||||
<tr
|
||||
v-for="row in pagedIncidentRows"
|
||||
:key="row.id"
|
||||
class="transition-colors duration-150 hover:bg-brand-gray-light focus-visible:bg-gray-100/70"
|
||||
class="transition-colors duration-150 hover:bg-brand-gray-lighter focus-visible:bg-gray-100/70"
|
||||
>
|
||||
<td>{{ row.time }}</td>
|
||||
<td class="truncate" :title="row.org">{{ row.org }}</td>
|
||||
<td class="truncate" :title="row.event">{{ row.event }}</td>
|
||||
|
||||
<td class="text-center">
|
||||
<button
|
||||
class="btn btn-link btn-xs px-0 text-brand-purple-dark !no-underline hover:!no-underline hover:opacity-80 focus-visible:!no-underline"
|
||||
@click="viewDetailIncident(row)"
|
||||
>
|
||||
查看詳情
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -299,26 +281,26 @@
|
||||
|
||||
<!-- 分頁(固定在底) -->
|
||||
<div class="mt-3 flex items-center justify-between">
|
||||
<span class="ml-3 text-sm text-gray-500">
|
||||
<span class="ml-3 text-sm text-brand-gray">
|
||||
共 {{ incidentTotal }} 筆,每頁 {{ incidentPageSize }} 筆
|
||||
</span>
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple disabled:!opacity-100 disabled:!text-gray-300 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="incidentPage === 1"
|
||||
@click="incidentPage--"
|
||||
>
|
||||
上一頁
|
||||
</button>
|
||||
|
||||
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
|
||||
{{ currentPage }} / {{ totalPages }}
|
||||
{{ incidentPage }} / {{ incidentTotalPages }}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="currentPage++"
|
||||
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple disabled:!opacity-100 disabled:!text-gray-300 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="incidentPage === incidentTotalPages"
|
||||
@click="incidentPage++"
|
||||
>
|
||||
下一頁
|
||||
</button>
|
||||
@ -327,22 +309,21 @@
|
||||
</section>
|
||||
<!-- 表格 今日派車總表 -->
|
||||
<section
|
||||
class="bg-white/50 rounded-md shadow p-3 flex flex-col min-h-0 gap-3"
|
||||
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-3"
|
||||
>
|
||||
<h3 class="text-xl font-bold">今日派車總表</h3>
|
||||
<h3 class="text-2xl font-bold">今日派車總表</h3>
|
||||
|
||||
<!-- 可捲動內容區(水平) -->
|
||||
<div class="flex flex-col gap-4 mb-6">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="table whitespace-nowrap">
|
||||
<thead
|
||||
class="bg-brand-gray-light text-brand-black sticky top-0 z-[1]"
|
||||
class="bg-brand-gray-lighter text-brand-black sticky top-0 z-[1]"
|
||||
>
|
||||
<tr>
|
||||
<th class="w-[120px]">時間</th>
|
||||
<th class="w-[160px]">機構</th>
|
||||
<th class="w-[120px]">聯絡人</th>
|
||||
<th class="w-[120px] text-center">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -350,21 +331,13 @@
|
||||
<tr
|
||||
v-for="row in pagedDispatchRows"
|
||||
:key="row.id"
|
||||
class="transition-colors duration-150 hover:bg-brand-gray-light focus-visible:bg-gray-100/70"
|
||||
class="transition-colors duration-150 hover:bg-brand-gray-lighter focus-visible:bg-gray-100/70"
|
||||
>
|
||||
<td>{{ row.time }}</td>
|
||||
<td class="truncate" :title="row.org">{{ row.org }}</td>
|
||||
<td class="truncate" :title="row.contact">
|
||||
{{ row.contact }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button
|
||||
class="btn btn-link btn-xs px-0 text-brand-purple-dark !no-underline hover:!no-underline hover:opacity-80 focus-visible:!no-underline"
|
||||
@click="viewDispatchDetail(row)"
|
||||
>
|
||||
查看詳情
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -373,26 +346,26 @@
|
||||
|
||||
<!-- 分頁(固定在底) -->
|
||||
<div class="mt-3 flex items-center justify-between">
|
||||
<span class="ml-3 text-sm text-gray-500">
|
||||
<span class="ml-3 text-sm text-brand-gray">
|
||||
共 {{ dispatchTotal }} 筆,每頁 {{ dispatchPageSize }} 筆
|
||||
</span>
|
||||
<div class="inline-flex items-center">
|
||||
<button
|
||||
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple disabled:!opacity-100 disabled:!text-gray-300 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="dispatchPage === 1"
|
||||
@click="dispatchPage--"
|
||||
>
|
||||
上一頁
|
||||
</button>
|
||||
|
||||
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
|
||||
{{ currentPage }} / {{ totalPages }}
|
||||
{{ dispatchPage }} / {{ dispatchTotalPages }}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-outline disabled:!opacity-100 disabled:!text-gray-500 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="currentPage++"
|
||||
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple disabled:!opacity-100 disabled:!text-gray-300 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="dispatchPage === dispatchTotalPages"
|
||||
@click="dispatchPage++"
|
||||
>
|
||||
下一頁
|
||||
</button>
|
||||
@ -466,24 +439,48 @@ const progressTitleMap = {
|
||||
moveout: { current: 8, total: 50 },
|
||||
};
|
||||
|
||||
// 1) 共用:主標+副標 helper(可放檔案上方 utilities 區)
|
||||
function mkTitle(text, subtext = "", opts = {}) {
|
||||
return {
|
||||
text,
|
||||
subtext,
|
||||
left: "center",
|
||||
top: 6, // 主標距容器頂端
|
||||
itemGap: 6, // 主標與副標的距離
|
||||
textStyle: {
|
||||
color: brand.black,
|
||||
fontSize: 20,
|
||||
fontWeight: 600,
|
||||
fontFamily: '"Noto Sans TC"',
|
||||
},
|
||||
subtextStyle: {
|
||||
color: brand.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
fontFamily: '"Noto Sans TC"',
|
||||
lineHeight: 20,
|
||||
},
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
// 2) 改造 updateChartByKey:把 legends 當作副標
|
||||
function updateChartByKey(key) {
|
||||
activeKey.value = key;
|
||||
const conf = chartDataMap[key];
|
||||
|
||||
const title = `${conf.legends[0]} / ${conf.legends[1]}`;
|
||||
const subTitle = "(近 7 天)";
|
||||
|
||||
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"',
|
||||
},
|
||||
},
|
||||
|
||||
// 主標+副標
|
||||
title: mkTitle(title, subTitle),
|
||||
|
||||
// 建議加個 grid.top,避免標題區升高後擠到圖身
|
||||
grid: { top: 70, containLabel: true },
|
||||
|
||||
legend: {
|
||||
data: conf.legends,
|
||||
bottom: 8,
|
||||
@ -493,15 +490,22 @@ function updateChartByKey(key) {
|
||||
itemGap: 24,
|
||||
textStyle: { color: brand.gray },
|
||||
},
|
||||
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "數量",
|
||||
nameGap: 12,
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
nameLocation: "middle",
|
||||
nameGap: 40,
|
||||
axisLine: { show: true },
|
||||
axisTick: { show: true },
|
||||
axisLabel: { color: brand.gray },
|
||||
splitLine: { show: true, lineStyle: { color: brand.grayLight } },
|
||||
splitArea: {
|
||||
show: true,
|
||||
areaStyle: { color: [brand.white, brand.grayLighter] },
|
||||
},
|
||||
},
|
||||
|
||||
series: [
|
||||
{
|
||||
name: conf.legends[0],
|
||||
@ -665,32 +669,61 @@ onMounted(() => {
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- 第一列:icon + 名稱-->
|
||||
<div class="inline-flex justify-start items-center text-brand-purple-dark font-noto gap-1">
|
||||
<div class="inline-flex justify-between items-center text-brand-purple-dark font-noto">
|
||||
<div class="inline-flex justify-start items-center gap-1">
|
||||
<span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M5 19v-8.692q0-.384.172-.727t.474-.565l5.385-4.078q.423-.323.966-.323t.972.323l5.385 4.077q.303.222.474.566q.172.343.172.727V19q0 .402-.299.701T18 20h-3.384q-.344 0-.576-.232q-.232-.233-.232-.576v-4.769q0-.343-.232-.575q-.233-.233-.576-.233h-2q-.343 0-.575.233q-.233.232-.233.575v4.77q0 .343-.232.575T9.385 20H6q-.402 0-.701-.299T5 19"/>
|
||||
</svg>
|
||||
</span>
|
||||
<strong class="text-[14px]">${p.name}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-brand-gray/50">
|
||||
<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>
|
||||
|
||||
<!-- 第二列:統計卡群 -->
|
||||
<div class="flex justify-center items-center gap-1">
|
||||
<div class="flex flex-col justify-center items-center gap-1 border p-2 rounded-md">
|
||||
<div class="text-[16px]"><strong>住民</strong></div>
|
||||
<div class="font-nats text-xl text-brand-purple-dark"><p>36 / 49</p></div>
|
||||
</div>
|
||||
|
||||
<div class="inline-block rounded-md border border-gray-300 overflow-hidden">
|
||||
<table class="min-w-[160px] border-collapse">
|
||||
<thead class="bg-brand-gray-lighter text-brand-black">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-center border-r border-gray-300">住民</th>
|
||||
<th class="px-3 py-2 text-center border-r border-gray-300">空床</th>
|
||||
<th class="px-3 py-2 text-center">住院</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="px-3 py-2 border-r border-gray-300">
|
||||
<span class="font-nats text-xl text-brand-purple-dark">36 / 49</span>
|
||||
</td>
|
||||
<td class="px-3 py-2 border-r border-gray-300">
|
||||
<span class="font-nats text-xl text-brand-purple-dark">12 / 49</span>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
<span class="font-nats text-xl text-brand-purple-dark">1 / 20</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex flex-col justify-center items-center gap-1 border p-2 rounded-md">
|
||||
<div class="text-[16px]"><strong>空床</strong></div>
|
||||
<div class="font-nats text-xl text-brand-purple-dark"><p>12 / 49</p></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center items-center gap-1 border p-2 rounded-md">
|
||||
<div class="text-[16px]"><strong>住院</strong></div>
|
||||
<div class="font-nats text-xl text-brand-purple-dark"><p>1 / 20</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
@ -698,7 +731,7 @@ onMounted(() => {
|
||||
permanent: true, // 常駐顯示
|
||||
direction: "top", // 顯示在 pin 上方
|
||||
offset: [0, -(ICON_H + TIP_GAP)],
|
||||
opacity: 0.9,
|
||||
opacity: 0.95,
|
||||
}
|
||||
);
|
||||
});
|
||||
@ -823,11 +856,6 @@ const pagedDispatchRows = computed(() => {
|
||||
const start = (dispatchPage.value - 1) * dispatchPageSize;
|
||||
return dispatchRows.value.slice(start, start + dispatchPageSize);
|
||||
});
|
||||
|
||||
// 操作:查看詳情
|
||||
function viewDispatchDetail(row) {
|
||||
console.log("派車詳情:", row);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<section
|
||||
class="flex flex-col gap-2 h-[calc(100vh-72px-24px)] overflow-hidden"
|
||||
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden text-brand-black"
|
||||
>
|
||||
<!-- 高度比重:2 -->
|
||||
<div class="flex-[2] grid grid-cols-4 gap-2">
|
||||
<!-- 高度比重:2 -->
|
||||
<section class="flex-[2] grid grid-cols-3 gap-2">
|
||||
<!-- 住民人數 Card -->
|
||||
<div
|
||||
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"
|
||||
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">
|
||||
<p class="text-sm">住民人數</p>
|
||||
<p class="text-sm">現在住民/立案床數</p>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -28,11 +30,14 @@
|
||||
<p class="text-[12px]">人</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 住院人數 Card -->
|
||||
<div
|
||||
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"
|
||||
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">
|
||||
<p class="text-sm">住院人數</p>
|
||||
<p class="text-sm">今日住院/當月累積住院</p>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -52,11 +57,152 @@
|
||||
<p class="text-[12px]">人</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Modal(放在同一個 <template> 中,建議貼在最底部)===== -->
|
||||
<div
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<!-- footer -->
|
||||
<div class="mt-6 text-right">
|
||||
<button
|
||||
class="btn bg-brand-green text-white px-4 py-2 rounded-md"
|
||||
@click="closeModal"
|
||||
>
|
||||
關閉
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
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>
|
||||
<p class="text-sm">今日離院/當月累積離院</p>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -76,316 +222,506 @@
|
||||
<p class="text-[12px]">人</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<!-- 高度比重: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-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>
|
||||
<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-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>
|
||||
<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-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>
|
||||
<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-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>
|
||||
<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-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-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="chartBRef" class="w-[90%] h-[90%]"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高度比重:5 -->
|
||||
<div
|
||||
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]"
|
||||
<!-- 高度比重:10 -->
|
||||
<!-- 未完成的知會事項與代辦事項 -->
|
||||
<section
|
||||
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-4 flex-[10]"
|
||||
>
|
||||
<p>暫無內容</p>
|
||||
<!-- <h4 class="text-xl text-gray-600 font-bold">與去年同期比較</h4>
|
||||
<div ref="chartCRef" class="w-[90%] h-[90%]"></div> -->
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold">未完成的知會事項與代辦事項</h3>
|
||||
<!-- 可捲動內容區(水平) -->
|
||||
<div class="flex flex-col gap-4 mb-6 flex-1 min-h-0">
|
||||
<div class="w-full flex-1 min-h-0 overflow-x-auto overflow-y-auto">
|
||||
<table class="table w-full whitespace-nowrap">
|
||||
<thead
|
||||
class="bg-brand-gray-lighter text-brand-black sticky top-0 z-[1]"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">住民</th>
|
||||
<th class="px-3 py-2 text-left">類型</th>
|
||||
<th class="px-3 py-2 text-left">表單</th>
|
||||
<th class="px-3 py-2 text-left">日期</th>
|
||||
<th class="px-3 py-2 text-left">逾期天數</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in pagedRows"
|
||||
:key="row.id"
|
||||
class="transition-colors duration-150 hover:bg-brand-gray-lighter focus-visible:bg-gray-100/70"
|
||||
>
|
||||
<td class="truncate">{{ row.resident }}</td>
|
||||
<td>{{ row.type }}</td>
|
||||
<td class="truncate">{{ row.form }}</td>
|
||||
<td>{{ formatDate(row.date) }}</td>
|
||||
<td class="text-brand-red">{{ row.overdueDays ?? 0 }} 天</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分頁 -->
|
||||
<div class="mt-3 flex items-center justify-between px-3">
|
||||
<span class="text-sm text-gray-500">
|
||||
共 {{ total }} 筆,每頁 {{ pageSize }} 筆
|
||||
</span>
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple disabled:!opacity-100 disabled:!text-gray-300 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
>
|
||||
上一頁
|
||||
</button>
|
||||
|
||||
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
|
||||
{{ currentPage }} / {{ totalPages }}
|
||||
</span>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-outline border-brand-purple-dark text-brand-purple-dark hover:bg-brand-purple hover:text-white hover:border-brand-purple disabled:!opacity-100 disabled:!text-gray-300 disabled:!border-gray-300 disabled:cursor-not-allowed"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="currentPage++"
|
||||
>
|
||||
下一頁
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
import { brand } from "@/styles/palette";
|
||||
// 你原本的 import 改成也帶入 watch
|
||||
|
||||
// ---- DOM Refs ----
|
||||
const chartARef = ref(null);
|
||||
const chartBRef = ref(null);
|
||||
const chartCRef = ref(null);
|
||||
/* =========================
|
||||
未完成的知會事項與代辦事項
|
||||
- 10 筆/頁
|
||||
- 分頁邏輯+邊界保護
|
||||
- 假資料欄位與表頭一致
|
||||
========================= */
|
||||
|
||||
// ---- Chart instances ----
|
||||
let chartA, chartB, chartC;
|
||||
// 預設每頁 10 筆
|
||||
const pageSize = ref(10);
|
||||
const currentPage = ref(1);
|
||||
|
||||
// 假資料工具
|
||||
function genTrend(len, start = 50, drift = 0.6, noise = 20) {
|
||||
// 假資料(實務上你可以改用 API 回來的陣列)
|
||||
const todoRows = ref([
|
||||
// id、住民、類型、表單、日期、預期天數
|
||||
{
|
||||
id: 1,
|
||||
resident: "王小明",
|
||||
type: "醫囑簽章",
|
||||
form: "用藥知會單",
|
||||
date: "2025-08-26",
|
||||
overdueDays: 3,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
resident: "陳美麗",
|
||||
type: "護理評估",
|
||||
form: "入院評估表",
|
||||
date: "2025-08-27",
|
||||
overdueDays: 5,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
resident: "林大志",
|
||||
type: "家屬同意",
|
||||
form: "手術同意書",
|
||||
date: "2025-08-28",
|
||||
overdueDays: 2,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
resident: "張翠華",
|
||||
type: "衛教回覆",
|
||||
form: "跌倒防護衛教",
|
||||
date: "2025-08-28",
|
||||
overdueDays: 7,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
resident: "黃國榮",
|
||||
type: "復健紀錄",
|
||||
form: "PT 日誌",
|
||||
date: "2025-08-29",
|
||||
overdueDays: 4,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
resident: "李佩珊",
|
||||
type: "家屬同意",
|
||||
form: "外出同意書",
|
||||
date: "2025-08-29",
|
||||
overdueDays: 3,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
resident: "吳大同",
|
||||
type: "醫囑簽章",
|
||||
form: "換藥醫囑",
|
||||
date: "2025-08-30",
|
||||
overdueDays: 2,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
resident: "周怡君",
|
||||
type: "護理評估",
|
||||
form: "壓瘡評估表",
|
||||
date: "2025-08-30",
|
||||
overdueDays: 5,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
resident: "曾文龍",
|
||||
type: "家屬同意",
|
||||
form: "轉院同意書",
|
||||
date: "2025-08-31",
|
||||
overdueDays: 1,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
resident: "蔡淑芬",
|
||||
type: "醫囑簽章",
|
||||
form: "抽血醫囑",
|
||||
date: "2025-08-31",
|
||||
overdueDays: 3,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
resident: "許建宏",
|
||||
type: "衛教回覆",
|
||||
form: "糖尿病衛教單",
|
||||
date: "2025-09-01",
|
||||
overdueDays: 6,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
resident: "簡婉婷",
|
||||
type: "復健紀錄",
|
||||
form: "OT 日誌",
|
||||
date: "2025-09-01",
|
||||
overdueDays: 4,
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
resident: "王小明",
|
||||
type: "醫囑簽章",
|
||||
form: "檢驗醫囑",
|
||||
date: "2025-09-01",
|
||||
overdueDays: 2,
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
resident: "陳美麗",
|
||||
type: "護理評估",
|
||||
form: "疼痛評估表",
|
||||
date: "2025-09-01",
|
||||
overdueDays: 3,
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
resident: "林大志",
|
||||
type: "家屬同意",
|
||||
form: "管路同意書",
|
||||
date: "2025-09-01",
|
||||
overdueDays: 5,
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
resident: "張翠華",
|
||||
type: "衛教回覆",
|
||||
form: "跌倒防護衛教",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 7,
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
resident: "黃國榮",
|
||||
type: "復健紀錄",
|
||||
form: "PT 日誌",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 4,
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
resident: "李佩珊",
|
||||
type: "家屬同意",
|
||||
form: "手術同意書",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 2,
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
resident: "吳大同",
|
||||
type: "醫囑簽章",
|
||||
form: "換藥醫囑",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 3,
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
resident: "周怡君",
|
||||
type: "護理評估",
|
||||
form: "入院評估表",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 5,
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
resident: "曾文龍",
|
||||
type: "家屬同意",
|
||||
form: "外出同意書",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 1,
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
resident: "蔡淑芬",
|
||||
type: "醫囑簽章",
|
||||
form: "抽血醫囑",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 3,
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
resident: "許建宏",
|
||||
type: "衛教回覆",
|
||||
form: "糖尿病衛教單",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 6,
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
resident: "簡婉婷",
|
||||
type: "復健紀錄",
|
||||
form: "OT 日誌",
|
||||
date: "2025-09-02",
|
||||
overdueDays: 4,
|
||||
},
|
||||
]);
|
||||
|
||||
// 總筆數 / 總頁數
|
||||
const total = computed(() => todoRows.value.length);
|
||||
const totalPages = computed(() =>
|
||||
Math.max(1, Math.ceil(total.value / pageSize.value))
|
||||
);
|
||||
|
||||
// 目前頁的切片資料
|
||||
const pagedRows = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value;
|
||||
return todoRows.value.slice(start, start + pageSize.value);
|
||||
});
|
||||
|
||||
// 分頁邊界保護(例:刪到只剩 1 頁時避免 currentPage 超出)
|
||||
watch([totalPages, currentPage], () => {
|
||||
if (currentPage.value > totalPages.value)
|
||||
currentPage.value = totalPages.value;
|
||||
if (currentPage.value < 1) currentPage.value = 1;
|
||||
});
|
||||
|
||||
// 小工具:日期格式 YYYY-MM-DD
|
||||
function formatDate(d) {
|
||||
const dt = typeof d === "string" ? new Date(d) : d;
|
||||
if (Number.isNaN(dt.getTime())) return String(d ?? "");
|
||||
const y = dt.getFullYear();
|
||||
const m = String(dt.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(dt.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
/* ===== 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 v = start;
|
||||
for (let i = 0; i < len; i++) {
|
||||
v = v + drift + (Math.random() * noise * 2 - noise);
|
||||
arr.push(Number(v.toFixed(1)));
|
||||
let n = start;
|
||||
while (arr.length < count) {
|
||||
if (!String(n).includes("4")) arr.push(n);
|
||||
n++;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const commonGrid = {
|
||||
left: 40,
|
||||
right: 20,
|
||||
top: 50,
|
||||
bottom: 30,
|
||||
containLabel: true,
|
||||
};
|
||||
const commonTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
|
||||
/* ===== 假資料:住民 ===== */
|
||||
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",
|
||||
},
|
||||
];
|
||||
|
||||
// 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),
|
||||
}));
|
||||
const residentsA = residentsAll.filter((r) => r.area === "A");
|
||||
const residentsB = residentsAll.filter((r) => r.area === "B");
|
||||
|
||||
return {
|
||||
legend: { top: 10 },
|
||||
tooltip: commonTooltip,
|
||||
grid: commonGrid,
|
||||
xAxis: { type: "category", data: x },
|
||||
yAxis: { type: "value" },
|
||||
// 指定顏色
|
||||
color: [brand.green, brand.greenLight, brand.yellow],
|
||||
series,
|
||||
};
|
||||
/* ===== 假資料:住院 ===== */
|
||||
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();
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
onMounted(() => window.addEventListener("keydown", onKeydown));
|
||||
onUnmounted(() => window.removeEventListener("keydown", onKeydown));
|
||||
</script>
|
||||
|
@ -3,15 +3,15 @@
|
||||
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="flex-[2] grid grid-cols-3 gap-2">
|
||||
<!-- 住民人數 Card -->
|
||||
<div
|
||||
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"
|
||||
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">
|
||||
<p class="text-sm">住民人數</p>
|
||||
<span>
|
||||
<p class="text-sm">現在住民/立案床數</p>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@ -33,12 +33,12 @@
|
||||
|
||||
<!-- 住院人數 Card -->
|
||||
<div
|
||||
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"
|
||||
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">
|
||||
<p class="text-sm">住院人數</p>
|
||||
<span>
|
||||
<p class="text-sm">今日住院/當月累積住院</p>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
@ -61,7 +61,7 @@
|
||||
<!-- ===== Modal(放在同一個 <template> 中,建議貼在最底部)===== -->
|
||||
<div
|
||||
v-if="showModal"
|
||||
class="fixed inset-0 z-[100] flex items-center justify-center"
|
||||
class="fixed inset-0 z-[90] flex items-center justify-center"
|
||||
@keydown.esc="closeModal"
|
||||
tabindex="0"
|
||||
>
|
||||
@ -95,7 +95,7 @@
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm text-brand-black">
|
||||
<thead>
|
||||
<tr class="text-left bg-gray-50">
|
||||
<tr class="text-left bg-brand-gray-lighter">
|
||||
<th class="px-3 py-2">床位</th>
|
||||
<th class="px-3 py-2">姓名</th>
|
||||
<th class="px-3 py-2">性別</th>
|
||||
@ -108,7 +108,7 @@
|
||||
<tr
|
||||
v-for="r in residentsA"
|
||||
:key="r.bed"
|
||||
class="border-b last:border-b-0 hover:bg-gray-50"
|
||||
class="border-b last:border-b-0 hover:bg-brand-gray-lighter"
|
||||
>
|
||||
<td class="px-3 py-2 font-mono">{{ r.bed }}</td>
|
||||
<td class="px-3 py-2">{{ r.name }}</td>
|
||||
@ -127,7 +127,7 @@
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm text-brand-black">
|
||||
<thead>
|
||||
<tr class="text-left bg-gray-50">
|
||||
<tr class="text-left bg-brand-gray-lighter">
|
||||
<th class="px-3 py-2">床位</th>
|
||||
<th class="px-3 py-2">姓名</th>
|
||||
<th class="px-3 py-2">性別</th>
|
||||
@ -140,7 +140,7 @@
|
||||
<tr
|
||||
v-for="r in residentsB"
|
||||
:key="r.bed"
|
||||
class="border-b last:border-b-0 hover:bg-gray-50"
|
||||
class="border-b last:border-b-0 hover:bg-brand-gray-lighter"
|
||||
>
|
||||
<td class="px-3 py-2 font-mono">{{ r.bed }}</td>
|
||||
<td class="px-3 py-2">{{ r.name }}</td>
|
||||
@ -157,7 +157,6 @@
|
||||
|
||||
<!-- 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>
|
||||
@ -189,7 +188,7 @@
|
||||
<!-- footer -->
|
||||
<div class="mt-6 text-right">
|
||||
<button
|
||||
class="btn bg-brand-green text-white px-4 py-2 rounded-md "
|
||||
class="btn bg-brand-green text-white border-none px-4 py-2 rounded-md"
|
||||
@click="closeModal"
|
||||
>
|
||||
關閉
|
||||
@ -199,34 +198,10 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
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"
|
||||
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>
|
||||
<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-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>
|
||||
<p class="text-sm">今日離院/當月累積離院</p>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -250,26 +225,26 @@
|
||||
|
||||
<!-- 高度比重:5 -->
|
||||
<div class="flex-[5] grid grid-cols-2 gap-2">
|
||||
<!-- 佔床率 -->
|
||||
<div
|
||||
class="col-span-1 bg-white bg-opacity-70 rounded-md shadow p-5 flex flex-col justify-center items-start"
|
||||
class="col-span-1 bg-white bg-opacity-70 rounded-md shadow p-2 flex justify-center items-center"
|
||||
>
|
||||
<h4 class="text-xl text-gray-600 font-bold">近三個月比較</h4>
|
||||
<div ref="chartARef" class="w-full h-full"></div>
|
||||
<div ref="chartARef" class="w-full h-[90%] min-h-[200px]"></div>
|
||||
</div>
|
||||
<!-- 住院率 -->
|
||||
<div
|
||||
class="col-span-1 bg-white bg-opacity-70 rounded-md shadow p-5 flex flex-col justify-center items-start"
|
||||
class="col-span-1 bg-white bg-opacity-70 rounded-md shadow p-2 flex justify-center items-center"
|
||||
>
|
||||
<h4 class="text-xl text-gray-600 font-bold">近六個月比較</h4>
|
||||
<div ref="chartBRef" class="w-full h-full"></div>
|
||||
<div ref="chartBRef" class="w-full h-[90%] min-h-[200px]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高度比重:5 -->
|
||||
<div
|
||||
class="bg-white bg-opacity-70 rounded-md shadow p-5 flex flex-col justify-center items-start flex-[5]"
|
||||
class="bg-white bg-opacity-70 rounded-md shadow p-2 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 ref="chartCRef" class="w-full h-[90%] min-h-[200px]"></div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@ -298,6 +273,89 @@ function genTrend(len, start = 50, drift = 0.6, noise = 20) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
// 生成百分比(0~100)資料陣列,base 為平均,variation 為波動幅度
|
||||
function genRates(n, base = 82, variation = 10) {
|
||||
const clamp = (v) => Math.max(0, Math.min(100, v));
|
||||
const arr = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const noise = (Math.random() * 2 - 1) * variation;
|
||||
arr.push(Number(clamp(base + noise).toFixed(1)));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// 取得最近 N 個月份標籤(含當月),格式:["4月","5月","6月","7月","8月","9月"]
|
||||
function getLastNMonthLabels(n = 6) {
|
||||
const now = new Date();
|
||||
const labels = [];
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
labels.push(`${d.getMonth() + 1}月`);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
// 量化工具:線性插值分位數
|
||||
function quantile(sorted, q) {
|
||||
const pos = (sorted.length - 1) * q;
|
||||
const base = Math.floor(pos);
|
||||
const rest = pos - base;
|
||||
return sorted[base + 1] !== undefined
|
||||
? sorted[base] + rest * (sorted[base + 1] - sorted[base])
|
||||
: sorted[base];
|
||||
}
|
||||
|
||||
// 將多組樣本 => 箱型圖資料([min, Q1, median, Q3, max])與離群點 [[xIndex, value], ...]
|
||||
function toBoxplot(groups) {
|
||||
const boxData = [];
|
||||
const outliers = [];
|
||||
|
||||
groups.forEach((arr, idx) => {
|
||||
const sorted = [...arr].sort((a, b) => a - b);
|
||||
const q1 = quantile(sorted, 0.25);
|
||||
const med = quantile(sorted, 0.5);
|
||||
const q3 = quantile(sorted, 0.75);
|
||||
const iqr = q3 - q1;
|
||||
const lowerFence = q1 - 1.5 * iqr;
|
||||
const upperFence = q3 + 1.5 * iqr;
|
||||
|
||||
const inside = sorted.filter((v) => v >= lowerFence && v <= upperFence);
|
||||
const min = inside.length ? inside[0] : q1; // 防守
|
||||
const max = inside.length ? inside[inside.length - 1] : q3;
|
||||
|
||||
// 收集離群點
|
||||
sorted.forEach((v) => {
|
||||
if (v < lowerFence || v > upperFence) outliers.push([idx, v]);
|
||||
});
|
||||
|
||||
boxData.push([
|
||||
+min.toFixed(1),
|
||||
+q1.toFixed(1),
|
||||
+med.toFixed(1),
|
||||
+q3.toFixed(1),
|
||||
+max.toFixed(1),
|
||||
]);
|
||||
});
|
||||
|
||||
return { boxData, outliers };
|
||||
}
|
||||
|
||||
// 箱型圖 BoxPlot
|
||||
function makeBoxGroups(
|
||||
groupLabels,
|
||||
pointsPerGroup = 30,
|
||||
base = 82,
|
||||
step = -1.5
|
||||
) {
|
||||
const raw = groupLabels.map((_, i) =>
|
||||
genRates(pointsPerGroup, base + i * step, 12)
|
||||
);
|
||||
const means = raw.map((arr) =>
|
||||
Number((arr.reduce((a, b) => a + b, 0) / arr.length).toFixed(1))
|
||||
);
|
||||
return { raw, means };
|
||||
}
|
||||
|
||||
const commonGrid = {
|
||||
left: 40,
|
||||
right: 20,
|
||||
@ -307,87 +365,241 @@ const commonGrid = {
|
||||
};
|
||||
const commonTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
|
||||
|
||||
// A:三個月趨勢
|
||||
function mkTitle(text, subtext = "", opts = {}) {
|
||||
const useInline = !!subtext;
|
||||
|
||||
if (useInline) {
|
||||
return {
|
||||
text: `{main|${text}} {sub|${subtext}}`,
|
||||
left: 8,
|
||||
top: 2,
|
||||
textStyle: {
|
||||
rich: {
|
||||
main: {
|
||||
color: brand.black,
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
fontFamily: '"Noto Sans TC"',
|
||||
},
|
||||
sub: {
|
||||
color: brand.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
fontFamily: '"Noto Sans TC"',
|
||||
align: "left",
|
||||
},
|
||||
},
|
||||
},
|
||||
subtext: "",
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
subtext: "",
|
||||
textStyle: {
|
||||
color: brand.black,
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
fontFamily: '"Noto Sans TC"',
|
||||
},
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const baseGrid = {
|
||||
top: 48,
|
||||
right: 16,
|
||||
bottom: 24,
|
||||
left: 24,
|
||||
containLabel: true,
|
||||
};
|
||||
|
||||
// 佔床率(近六個月 → 箱型圖 + 平均點)
|
||||
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)) // 加大偏移與震盪
|
||||
const labels = getLastNMonthLabels(6); // 近六個月
|
||||
// 產生六組樣本(每組 30 筆、約 80~92%),並限制在 0~100 之間
|
||||
const raw = labels.map((_, i) =>
|
||||
genRates(30, 88 - i * 1.5, 10).map((v) => Math.max(0, Math.min(100, v)))
|
||||
);
|
||||
|
||||
const { boxData, outliers } = toBoxplot(raw);
|
||||
const BOX = brand.purpleDark;
|
||||
|
||||
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],
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: (p) => {
|
||||
if (p.seriesType === "boxplot") {
|
||||
const [min, med, max] = p.data;
|
||||
return [
|
||||
`<b>${labels[p.dataIndex]} 佔床率</b>`,
|
||||
`最高:${max}%`,
|
||||
`中位數:${med}%`,
|
||||
`最低:${min}%`,
|
||||
// `<span style="opacity:.6">(離群點依 1.5×IQR 計)</span>`,
|
||||
].join("<br/>");
|
||||
}
|
||||
// if (p.seriesType === "scatter") {
|
||||
// return `<b>${labels[p.data[0]]}</b><br/>離群點:${p.data[1]}%`;
|
||||
// }
|
||||
return "";
|
||||
},
|
||||
},
|
||||
title: mkTitle("佔床率", "(近 6 個月)"),
|
||||
grid: { ...commonGrid, top: 60 },
|
||||
xAxis: { type: "category", data: labels },
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "比例",
|
||||
nameLocation: "middle", // 置中(預設 y 軸會旋轉 90 度)
|
||||
nameGap: 45,
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: { formatter: "{value}%" },
|
||||
axisLine: { show: true },
|
||||
axisTick: { show: true },
|
||||
splitArea: {
|
||||
show: true,
|
||||
areaStyle: { color: [brand.white, brand.grayLighter] },
|
||||
}, // 淡淡分帶
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "去年",
|
||||
type: "line",
|
||||
smooth: true,
|
||||
data: lastYear,
|
||||
name: "箱型圖",
|
||||
type: "boxplot",
|
||||
data: boxData,
|
||||
itemStyle: { color: brand.greenLight, borderColor: BOX },
|
||||
lineStyle: { color: BOX },
|
||||
},
|
||||
{ name: "今年", type: "line", smooth: true, data: thisYear },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// 住院率(近六個月 → 箱型圖 + 平均點)
|
||||
function buildOptionB() {
|
||||
const labels = getLastNMonthLabels(6); // 近六個月
|
||||
// 住院率通常低許多:每組 30 筆、約 6~14%,並限制在 0~40
|
||||
const raw = labels.map((_, i) =>
|
||||
genRates(30, 8 + i * 1.2, 6).map((v) => Math.max(0, Math.min(40, v)))
|
||||
);
|
||||
|
||||
const { boxData, outliers } = toBoxplot(raw);
|
||||
const BOX = brand.purpleDark;
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: (p) => {
|
||||
if (p.seriesType === "boxplot") {
|
||||
const [min, med, max] = p.data;
|
||||
return [
|
||||
`<b>${labels[p.dataIndex]} 住院率</b>`,
|
||||
`最高:${max}%`,
|
||||
`中位數:${med}%`,
|
||||
`最低:${min}%`,
|
||||
// `<span style="opacity:.6">(離群點依 1.5×IQR 計)</span>`,
|
||||
].join("<br/>");
|
||||
}
|
||||
// if (p.seriesType === "scatter") {
|
||||
// return `<b>${labels[p.data[0]]}</b><br/>離群點:${p.data[1]}%`;
|
||||
// }
|
||||
return "";
|
||||
},
|
||||
},
|
||||
title: mkTitle("住院率", "(近 6 個月)"),
|
||||
grid: { ...commonGrid, top: 60 },
|
||||
xAxis: { type: "category", data: labels },
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "比例",
|
||||
nameLocation: "middle", // 置中(預設 y 軸會旋轉 90 度)
|
||||
nameGap: 45,
|
||||
min: 0,
|
||||
max: 40,
|
||||
axisLabel: { formatter: "{value}%" },
|
||||
axisLine: { show: true },
|
||||
axisTick: { show: true },
|
||||
splitArea: {
|
||||
show: true,
|
||||
areaStyle: { color: [brand.white, brand.grayLighter] },
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "箱型圖",
|
||||
type: "boxplot",
|
||||
data: boxData,
|
||||
itemStyle: { color: brand.greenLight, borderColor: BOX },
|
||||
lineStyle: { color: BOX },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// 每日佔床率比較(30 天 × 3 條線)
|
||||
function buildOptionC() {
|
||||
const days = Array.from({ length: 30 }, (_, i) => `${i + 1}日`);
|
||||
|
||||
const thisMonth = genRates(30, 86, 6).map((v, i) =>
|
||||
Number((v + Math.sin(i / 4) * 2).toFixed(1))
|
||||
);
|
||||
const lastMonth = genRates(30, 83, 7).map((v, i) =>
|
||||
Number((v + Math.cos(i / 5) * 2).toFixed(1))
|
||||
);
|
||||
const lastYearSameMonth = genRates(30, 80, 8).map((v, i) =>
|
||||
Number((v + Math.sin(i / 3) * 1.5).toFixed(1))
|
||||
);
|
||||
|
||||
// 想拉開更多距離:legend 往下放、grid 往下放
|
||||
const LEGEND_TOP = 28; // 原本 24 → 與 title 拉開
|
||||
const GRID_TOP = 64;
|
||||
|
||||
return {
|
||||
title: {
|
||||
...mkTitle("每日佔床率比較", "(近 30 天)"),
|
||||
top: 6,
|
||||
},
|
||||
legend: {
|
||||
top: LEGEND_TOP,
|
||||
data: ["機構 A", "本機構", "機構 B"],
|
||||
},
|
||||
tooltip: { trigger: "axis", valueFormatter: (v) => `${v}%` },
|
||||
|
||||
grid: {
|
||||
...commonGrid,
|
||||
top: GRID_TOP,
|
||||
containLabel: true,
|
||||
},
|
||||
|
||||
xAxis: { type: "category", data: days, boundaryGap: false },
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "比例",
|
||||
nameLocation: "middle",
|
||||
nameGap: 45,
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: { formatter: "{value}%" },
|
||||
axisLine: { show: true },
|
||||
axisTick: { show: true },
|
||||
splitArea: {
|
||||
show: true,
|
||||
areaStyle: { color: [brand.white, brand.grayLighter] },
|
||||
},
|
||||
},
|
||||
color: [brand.green, brand.purple, brand.yellow],
|
||||
series: [
|
||||
{ name: "機構 A", type: "line", data: thisMonth },
|
||||
{ name: "本機構", type: "line", data: lastMonth },
|
||||
{ name: "機構 B", type: "line", data: lastYearSameMonth },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 初始化 & resize
|
||||
function initCharts() {
|
||||
if (chartARef.value && !chartA) {
|
||||
|
@ -13,5 +13,7 @@ export const brand = {
|
||||
black: "#424242",
|
||||
gray: "#828282",
|
||||
grayLight: "#E9E9E9",
|
||||
grayLighter: "#F6F6F6",
|
||||
grayDark: "#D2D2D2",
|
||||
white: "#ffffff"
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ module.exports = {
|
||||
"2xl": "2.5rem",
|
||||
},
|
||||
screens: {
|
||||
// 讓 container 在各斷點對齊你自訂的寬度
|
||||
// 讓 container 在各斷點對齊自訂的寬度
|
||||
sm: "640px",
|
||||
md: "768px",
|
||||
lg: "1024px",
|
||||
@ -33,14 +33,14 @@ module.exports = {
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
// 讓你能寫 class="font-noto"
|
||||
// class="font-noto"
|
||||
noto: ["Noto Sans TC", "sans-serif"],
|
||||
// 讓你能寫 class="font-nats"
|
||||
// class="font-nats"
|
||||
nats: ["NATS-Regular", "sans-serif"],
|
||||
},
|
||||
colors: {
|
||||
brand: {
|
||||
green: { DEFAULT: "#34D5C8", light: "#C4FBE5", dark:"#0CA99C" },
|
||||
green: { DEFAULT: "#34D5C8", light: "#C4FBE5", dark: "#0CA99C" },
|
||||
red: "#FF8678",
|
||||
purple: {
|
||||
DEFAULT: "#A5BEFF",
|
||||
@ -54,6 +54,7 @@ module.exports = {
|
||||
black: "#424242",
|
||||
gray: {
|
||||
DEFAULT: "#828282",
|
||||
lighter: "#EEEEEE",
|
||||
light: "#E9E9E9",
|
||||
dark: "#D2D2D2",
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user