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>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
@ -389,7 +700,7 @@ function rebuildLabelsAfterNextRender() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ====== 分區切換(多選|預設不選=顯示全部) ======
|
// ====== 分區切換(多選|預設不選=顯示全部) ======
|
||||||
const zones = ["VIP", "換管", "失能", "失智"];
|
const zones = ["一般", "氧氣"];
|
||||||
const selectedZones = ref(new Set());
|
const selectedZones = ref(new Set());
|
||||||
const toggleZone = (zone) => {
|
const toggleZone = (zone) => {
|
||||||
const set = new Set(selectedZones.value);
|
const set = new Set(selectedZones.value);
|
||||||
@ -569,318 +880,6 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener("keydown", onKeydownInfo);
|
document.removeEventListener("keydown", onKeydownInfo);
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
<style>
|
||||||
.adsk-viewing-viewer {
|
.adsk-viewing-viewer {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
0
src/hooks/usePagination.js
Normal file
0
src/hooks/usePagination.js
Normal file
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav
|
<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 + 機構切換 -->
|
<!-- 左側:Logo + 機構切換 -->
|
||||||
<div
|
<div
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<img src="/img/logo.png" alt="Logo" class="h-full w-auto" />
|
<img src="/img/logo.png" alt="Logo" class="h-full w-auto" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative" v-if="!isHome">
|
||||||
<!-- 手機到桌機都顯示機構名稱,維持桌機尺寸 -->
|
<!-- 手機到桌機都顯示機構名稱,維持桌機尺寸 -->
|
||||||
<div
|
<div
|
||||||
ref="triggerRef"
|
ref="triggerRef"
|
||||||
@ -18,13 +18,13 @@
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
:aria-expanded="isOpen"
|
:aria-expanded="isFacilityOpen"
|
||||||
@click="toggle"
|
@click="toggleFacility"
|
||||||
@keydown.enter.prevent="toggle"
|
@keydown.enter.prevent="toggleFacility"
|
||||||
@keydown.space.prevent="toggle"
|
@keydown.space.prevent="toggleFacility"
|
||||||
>
|
>
|
||||||
<!-- 直接顯示,不再隱藏 -->
|
<!-- 直接顯示,不再隱藏 -->
|
||||||
<span>{{ displayLabel }}</span>
|
<span class="tracking-wider">{{ displayLabel }}</span>
|
||||||
<!-- caret icon -->
|
<!-- caret icon -->
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -38,9 +38,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dropdown(小螢幕靠左;桌機可改靠右) -->
|
<!-- Dropdown-->
|
||||||
<div
|
<div
|
||||||
v-show="isOpen"
|
v-show="isFacilityOpen"
|
||||||
ref="panelRef"
|
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"
|
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
|
@click.stop
|
||||||
@ -50,7 +50,7 @@
|
|||||||
v-for="item in facilities"
|
v-for="item in facilities"
|
||||||
:key="item"
|
:key="item"
|
||||||
@click="selectItem(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' : ''"
|
:class="selectedItem === item ? 'bg-brand-green-light' : ''"
|
||||||
>
|
>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
@ -508,28 +508,41 @@ import {
|
|||||||
onUnmounted,
|
onUnmounted,
|
||||||
defineOptions,
|
defineOptions,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
watch,
|
||||||
} from "vue";
|
} 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(機構清單)======
|
// ====== Dropdown(機構清單)======
|
||||||
const isOpen = ref(false);
|
const isFacilityOpen = ref(false);
|
||||||
const triggerRef = ref(null);
|
const triggerRef = ref(null);
|
||||||
const panelRef = ref(null);
|
const panelRef = ref(null);
|
||||||
|
|
||||||
const facilities = [
|
const facilities = [
|
||||||
"總部",
|
|
||||||
"護祐護理之家",
|
|
||||||
"崇祐護理之家",
|
|
||||||
"崇祐長照中心(養護型)",
|
|
||||||
"育祐護理之家",
|
|
||||||
"崇恩長照中心(養護型)",
|
|
||||||
"崇恩護理之家",
|
"崇恩護理之家",
|
||||||
|
"崇祐護理之家",
|
||||||
|
"護祐護理之家",
|
||||||
|
"育祐護理之家",
|
||||||
|
"崇智護理之家",
|
||||||
"崇恩居家護理所",
|
"崇恩居家護理所",
|
||||||
"傳心日間照顧中心",
|
"傳心日間照顧中心",
|
||||||
"敬慈居家服務中心",
|
"敬慈居家服務中心",
|
||||||
|
"崇恩長照中心(養護型)",
|
||||||
|
"崇祐長照中心(養護型)",
|
||||||
"傳祐長照中心(養護型)",
|
"傳祐長照中心(養護型)",
|
||||||
"中安崇恩長照中心(養護型)",
|
|
||||||
"崇智護理之家",
|
|
||||||
"慈祐長照中心(養護型)",
|
"慈祐長照中心(養護型)",
|
||||||
|
"中安崇恩長照中心(養護型)",
|
||||||
];
|
];
|
||||||
const selectedItem = ref(facilities[0]);
|
const selectedItem = ref(facilities[0]);
|
||||||
|
|
||||||
@ -543,8 +556,8 @@ const displayLabel = computed(() => {
|
|||||||
});
|
});
|
||||||
defineOptions({ name: "NavBar" });
|
defineOptions({ name: "NavBar" });
|
||||||
|
|
||||||
const toggle = () => {
|
const toggleFacility = () => {
|
||||||
isOpen.value = !isOpen.value;
|
isFacilityOpen.value = !isFacilityOpen.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickOutside = (e) => {
|
const onClickOutside = (e) => {
|
||||||
@ -553,7 +566,7 @@ const onClickOutside = (e) => {
|
|||||||
const clickedInsideTrigger = triggerRef.value.contains(t);
|
const clickedInsideTrigger = triggerRef.value.contains(t);
|
||||||
const clickedInsidePanel = panelRef.value.contains(t);
|
const clickedInsidePanel = panelRef.value.contains(t);
|
||||||
if (!clickedInsideTrigger && !clickedInsidePanel) {
|
if (!clickedInsideTrigger && !clickedInsidePanel) {
|
||||||
isOpen.value = false;
|
isFacilityOpen.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -573,8 +586,8 @@ const closeMobileMenu = () => {
|
|||||||
// ====== 全域鍵盤(Esc 同時關閉兩種面板)======
|
// ====== 全域鍵盤(Esc 同時關閉兩種面板)======
|
||||||
const handleKeydown = (e) => {
|
const handleKeydown = (e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
isOpen.value = false; // 關 dropdown
|
isFacilityOpen.value = false; // 關 dropdown
|
||||||
isMobileMenuOpen.value = false; // 關手機抽屜
|
isMobileMenuOpen.value = false; // 關 ham menu
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<progress
|
<progress
|
||||||
v-bind="$attrs"
|
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"
|
:class="heightClass"
|
||||||
:value="current"
|
:value="current"
|
||||||
:max="total"
|
:max="total"
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<!-- 上半:照片/說明 + 進度條(獨立 section) -->
|
<!-- 上半:照片/說明 + 進度條(獨立 section) -->
|
||||||
<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
|
<div
|
||||||
@ -21,9 +21,9 @@
|
|||||||
class="w-full h-full rounded-sm object-cover"
|
class="w-full h-full rounded-sm object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-span-4">
|
<div class="row-span-6">
|
||||||
<p
|
<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認證的長期照顧機構,集合了醫師群與資深護理群及照顧服務員們,在大南部地區照顧每一位需要我們的長輩。
|
崇恩長期照顧集團是國內第一家取得ISO認證的長期照顧機構,集合了醫師群與資深護理群及照顧服務員們,在大南部地區照顧每一位需要我們的長輩。
|
||||||
</p>
|
</p>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
|
|
||||||
<!-- Move-in -->
|
<!-- Move-in -->
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
label="今日新人入住/當月累積入住"
|
label="今日新入住/當月累積入住"
|
||||||
:current="12"
|
:current="12"
|
||||||
:total="50"
|
:total="50"
|
||||||
chartKey="movein"
|
chartKey="movein"
|
||||||
@ -78,12 +78,12 @@
|
|||||||
|
|
||||||
<!-- Move-out -->
|
<!-- Move-out -->
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
label="今日離院/累積離院"
|
label="今日離院/當月累積離院"
|
||||||
:current="8"
|
:current="8"
|
||||||
:total="50"
|
:total="50"
|
||||||
chartKey="moveout"
|
chartKey="moveout"
|
||||||
currentLegend="今日離院"
|
currentLegend="今日離院"
|
||||||
totalLegend="累積離院"
|
totalLegend="當月累積離院"
|
||||||
@select="({ key }) => updateChartByKey(key)"
|
@select="({ key }) => updateChartByKey(key)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +91,7 @@
|
|||||||
|
|
||||||
<!-- 下半:圖表(獨立 section,有內距) -->
|
<!-- 下半:圖表(獨立 section,有內距) -->
|
||||||
<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
|
<div
|
||||||
@ -103,7 +103,7 @@
|
|||||||
|
|
||||||
<!-- 葷/素資料 -->
|
<!-- 葷/素資料 -->
|
||||||
<section class="row-span-3 grid grid-cols-2 gap-2">
|
<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
|
<div
|
||||||
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-start items-center gap-3 p-3"
|
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>
|
||||||
</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
|
<div
|
||||||
class="border border-brand-yellow rounded-md w-full h-full flex flex-col justify-start items-center gap-3 p-3"
|
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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</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
|
<div
|
||||||
ref="mapEl"
|
ref="mapEl"
|
||||||
class="w-full rounded-md overflow-hidden min-h-[240px] sm:min-h-[300px] md:h-[420px] lg:h-full"
|
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"
|
style="aspect-ratio: 4 / 3"
|
||||||
></div>
|
></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 右側 -->
|
<!-- 右側 -->
|
||||||
<section class="flex flex-col gap-2 order-4 lg:order-3">
|
<section class="flex flex-col gap-2 order-4 lg:order-3">
|
||||||
<!-- 表格 今日活動 -->
|
<!-- 表格 今日活動 -->
|
||||||
<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="flex flex-col gap-4 mb-6">
|
||||||
<div class="w-full overflow-x-auto overflow-y-auto">
|
<div class="w-full overflow-x-auto overflow-y-auto">
|
||||||
<table class="table whitespace-nowrap">
|
<table class="table whitespace-nowrap">
|
||||||
<thead
|
<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>
|
<tr>
|
||||||
<th class="w-[120px]">時間</th>
|
<th class="w-[120px]">時間</th>
|
||||||
<th class="w-[160px]">機構</th>
|
<th class="w-[160px]">機構</th>
|
||||||
<th class="w-[160px]">活動名稱</th>
|
<th class="w-[160px]">活動名稱</th>
|
||||||
<th class="w-[120px] text-center">操作</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
v-for="row in pagedRows"
|
v-for="row in pagedRows"
|
||||||
:key="row.id"
|
: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.time }}</td>
|
||||||
<td>{{ row.org }}</td>
|
<td>{{ row.org }}</td>
|
||||||
<td class="truncate">{{ row.title }}</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -226,12 +219,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 分頁 -->
|
<!-- 分頁 -->
|
||||||
<div class="mt-3 flex items-center justify-between px-3">
|
<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 }} 筆
|
共 {{ total }} 筆,每頁 {{ pageSize }} 筆
|
||||||
</span>
|
</span>
|
||||||
<div class="inline-flex items-center gap-2">
|
<div class="inline-flex items-center gap-2">
|
||||||
<button
|
<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"
|
:disabled="currentPage === 1"
|
||||||
@click="currentPage--"
|
@click="currentPage--"
|
||||||
>
|
>
|
||||||
@ -243,7 +236,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<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"
|
:disabled="currentPage === totalPages"
|
||||||
@click="currentPage++"
|
@click="currentPage++"
|
||||||
>
|
>
|
||||||
@ -254,22 +247,20 @@
|
|||||||
</section>
|
</section>
|
||||||
<!-- 表格 今日異常事件 -->
|
<!-- 表格 今日異常事件 -->
|
||||||
<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="flex flex-col gap-4 mb-6">
|
||||||
<div class="w-full overflow-x-auto overflow-y-auto">
|
<div class="w-full overflow-x-auto overflow-y-auto">
|
||||||
<table class="table whitespace-nowrap">
|
<table class="table whitespace-nowrap">
|
||||||
<thead
|
<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>
|
<tr>
|
||||||
<th class="w-[100px]">時間</th>
|
<th class="w-[100px]">時間</th>
|
||||||
<th class="w-[160px]">機構</th>
|
<th class="w-[160px]">機構</th>
|
||||||
<th class="w-[100px]">事件</th>
|
<th class="w-[100px]">事件</th>
|
||||||
|
|
||||||
<th class="w-[120px] text-center">查看詳情</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@ -277,20 +268,11 @@
|
|||||||
<tr
|
<tr
|
||||||
v-for="row in pagedIncidentRows"
|
v-for="row in pagedIncidentRows"
|
||||||
:key="row.id"
|
: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.time }}</td>
|
||||||
<td class="truncate" :title="row.org">{{ row.org }}</td>
|
<td class="truncate" :title="row.org">{{ row.org }}</td>
|
||||||
<td class="truncate" :title="row.event">{{ row.event }}</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -299,26 +281,26 @@
|
|||||||
|
|
||||||
<!-- 分頁(固定在底) -->
|
<!-- 分頁(固定在底) -->
|
||||||
<div class="mt-3 flex items-center justify-between">
|
<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 }} 筆
|
共 {{ incidentTotal }} 筆,每頁 {{ incidentPageSize }} 筆
|
||||||
</span>
|
</span>
|
||||||
<div class="inline-flex items-center gap-2">
|
<div class="inline-flex items-center gap-2">
|
||||||
<button
|
<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"
|
:disabled="incidentPage === 1"
|
||||||
@click="currentPage--"
|
@click="incidentPage--"
|
||||||
>
|
>
|
||||||
上一頁
|
上一頁
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
|
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
|
||||||
{{ currentPage }} / {{ totalPages }}
|
{{ incidentPage }} / {{ incidentTotalPages }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<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"
|
:disabled="incidentPage === incidentTotalPages"
|
||||||
@click="currentPage++"
|
@click="incidentPage++"
|
||||||
>
|
>
|
||||||
下一頁
|
下一頁
|
||||||
</button>
|
</button>
|
||||||
@ -327,22 +309,21 @@
|
|||||||
</section>
|
</section>
|
||||||
<!-- 表格 今日派車總表 -->
|
<!-- 表格 今日派車總表 -->
|
||||||
<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="flex flex-col gap-4 mb-6">
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
<table class="table whitespace-nowrap">
|
<table class="table whitespace-nowrap">
|
||||||
<thead
|
<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>
|
<tr>
|
||||||
<th class="w-[120px]">時間</th>
|
<th class="w-[120px]">時間</th>
|
||||||
<th class="w-[160px]">機構</th>
|
<th class="w-[160px]">機構</th>
|
||||||
<th class="w-[120px]">聯絡人</th>
|
<th class="w-[120px]">聯絡人</th>
|
||||||
<th class="w-[120px] text-center">操作</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
@ -350,21 +331,13 @@
|
|||||||
<tr
|
<tr
|
||||||
v-for="row in pagedDispatchRows"
|
v-for="row in pagedDispatchRows"
|
||||||
:key="row.id"
|
: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.time }}</td>
|
||||||
<td class="truncate" :title="row.org">{{ row.org }}</td>
|
<td class="truncate" :title="row.org">{{ row.org }}</td>
|
||||||
<td class="truncate" :title="row.contact">
|
<td class="truncate" :title="row.contact">
|
||||||
{{ row.contact }}
|
{{ row.contact }}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -373,26 +346,26 @@
|
|||||||
|
|
||||||
<!-- 分頁(固定在底) -->
|
<!-- 分頁(固定在底) -->
|
||||||
<div class="mt-3 flex items-center justify-between">
|
<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 }} 筆
|
共 {{ dispatchTotal }} 筆,每頁 {{ dispatchPageSize }} 筆
|
||||||
</span>
|
</span>
|
||||||
<div class="inline-flex items-center">
|
<div class="inline-flex items-center">
|
||||||
<button
|
<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"
|
:disabled="dispatchPage === 1"
|
||||||
@click="currentPage--"
|
@click="dispatchPage--"
|
||||||
>
|
>
|
||||||
上一頁
|
上一頁
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
|
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
|
||||||
{{ currentPage }} / {{ totalPages }}
|
{{ dispatchPage }} / {{ dispatchTotalPages }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
<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"
|
:disabled="dispatchPage === dispatchTotalPages"
|
||||||
@click="currentPage++"
|
@click="dispatchPage++"
|
||||||
>
|
>
|
||||||
下一頁
|
下一頁
|
||||||
</button>
|
</button>
|
||||||
@ -466,24 +439,48 @@ const progressTitleMap = {
|
|||||||
moveout: { current: 8, total: 50 },
|
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) {
|
function updateChartByKey(key) {
|
||||||
activeKey.value = key;
|
activeKey.value = key;
|
||||||
const conf = chartDataMap[key];
|
const conf = chartDataMap[key];
|
||||||
|
|
||||||
|
const title = `${conf.legends[0]} / ${conf.legends[1]}`;
|
||||||
|
const subTitle = "(近 7 天)";
|
||||||
|
|
||||||
chart.setOption({
|
chart.setOption({
|
||||||
color: [brand.green, brand.purple],
|
color: [brand.green, brand.purple],
|
||||||
title: {
|
|
||||||
// 顯示 legend 名稱:現在住民/全立案床數
|
// 主標+副標
|
||||||
text: `${conf.legends[0]}/${conf.legends[1]}`,
|
title: mkTitle(title, subTitle),
|
||||||
left: "center",
|
|
||||||
top: 0,
|
// 建議加個 grid.top,避免標題區升高後擠到圖身
|
||||||
textStyle: {
|
grid: { top: 70, containLabel: true },
|
||||||
color: brand.gray,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
fontFamily: '"Noto Sans TC"',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
legend: {
|
||||||
data: conf.legends,
|
data: conf.legends,
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
@ -493,15 +490,22 @@ function updateChartByKey(key) {
|
|||||||
itemGap: 24,
|
itemGap: 24,
|
||||||
textStyle: { color: brand.gray },
|
textStyle: { color: brand.gray },
|
||||||
},
|
},
|
||||||
|
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
name: "數量",
|
name: "數量",
|
||||||
nameGap: 12,
|
nameLocation: "middle",
|
||||||
axisLine: { show: false },
|
nameGap: 40,
|
||||||
axisTick: { show: false },
|
axisLine: { show: true },
|
||||||
|
axisTick: { show: true },
|
||||||
axisLabel: { color: brand.gray },
|
axisLabel: { color: brand.gray },
|
||||||
splitLine: { show: true, lineStyle: { color: brand.grayLight } },
|
splitLine: { show: true, lineStyle: { color: brand.grayLight } },
|
||||||
|
splitArea: {
|
||||||
|
show: true,
|
||||||
|
areaStyle: { color: [brand.white, brand.grayLighter] },
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: conf.legends[0],
|
name: conf.legends[0],
|
||||||
@ -665,7 +669,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<!-- 第一列:icon + 名稱-->
|
<!-- 第一列: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>
|
<span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
<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"/>
|
<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"/>
|
||||||
@ -673,24 +678,52 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
<strong class="text-[14px]">${p.name}</strong>
|
<strong class="text-[14px]">${p.name}</strong>
|
||||||
</div>
|
</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="flex flex-col justify-center items-center gap-1 border p-2 rounded-md">
|
<div class="inline-block rounded-md border border-gray-300 overflow-hidden">
|
||||||
<div class="text-[16px]"><strong>空床</strong></div>
|
<table class="min-w-[160px] border-collapse">
|
||||||
<div class="font-nats text-xl text-brand-purple-dark"><p>12 / 49</p></div>
|
<thead class="bg-brand-gray-lighter text-brand-black">
|
||||||
</div>
|
<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>1 / 20</p></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
@ -698,7 +731,7 @@ onMounted(() => {
|
|||||||
permanent: true, // 常駐顯示
|
permanent: true, // 常駐顯示
|
||||||
direction: "top", // 顯示在 pin 上方
|
direction: "top", // 顯示在 pin 上方
|
||||||
offset: [0, -(ICON_H + TIP_GAP)],
|
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;
|
const start = (dispatchPage.value - 1) * dispatchPageSize;
|
||||||
return dispatchRows.value.slice(start, start + dispatchPageSize);
|
return dispatchRows.value.slice(start, start + dispatchPageSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 操作:查看詳情
|
|
||||||
function viewDispatchDetail(row) {
|
|
||||||
console.log("派車詳情:", row);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<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 -->
|
<!-- 高度比重:2 -->
|
||||||
<div class="flex-[2] grid grid-cols-4 gap-2">
|
<section class="flex-[2] grid grid-cols-3 gap-2">
|
||||||
|
<!-- 住民人數 Card -->
|
||||||
<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"
|
||||||
|
@click="openModal('residents')"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center items-center gap-4">
|
<div class="flex justify-center items-center gap-4">
|
||||||
<p class="text-sm">住民人數</p>
|
<p class="text-sm">現在住民/立案床數</p>
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -28,11 +30,14 @@
|
|||||||
<p class="text-[12px]">人</p>
|
<p class="text-[12px]">人</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 住院人數 Card -->
|
||||||
<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"
|
||||||
|
@click="openModal('inpatients')"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center items-center gap-4">
|
<div class="flex justify-center items-center gap-4">
|
||||||
<p class="text-sm">住院人數</p>
|
<p class="text-sm">今日住院/當月累積住院</p>
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -52,11 +57,152 @@
|
|||||||
<p class="text-[12px]">人</p>
|
<p class="text-[12px]">人</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== Modal(放在同一個 <template> 中,建議貼在最底部)===== -->
|
||||||
|
<div
|
||||||
|
v-if="showModal"
|
||||||
|
class="fixed inset-0 z-[100] flex items-center justify-center"
|
||||||
|
@keydown.esc="closeModal"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<!-- backdrop -->
|
||||||
|
<div class="absolute inset-0 bg-black/40" @click="closeModal"></div>
|
||||||
|
|
||||||
|
<!-- dialog -->
|
||||||
|
<div
|
||||||
|
class="relative bg-white w-[92vw] max-w-5xl max-h-[86vh] overflow-auto rounded-2xl shadow-xl px-12 py-8"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<!-- 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
|
<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">
|
<div class="flex justify-center items-center gap-4">
|
||||||
<p class="text-sm">其他人數</p>
|
<p class="text-sm">今日離院/當月累積離院</p>
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -76,316 +222,506 @@
|
|||||||
<p class="text-[12px]">人</p>
|
<p class="text-[12px]">人</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</section>
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- 高度比重:2 -->
|
<!-- 高度比重:10 -->
|
||||||
<div class="flex-[2] grid grid-cols-4 gap-2">
|
<!-- 未完成的知會事項與代辦事項 -->
|
||||||
<div
|
<section
|
||||||
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"
|
class="bg-white/70 rounded-md shadow p-6 flex flex-col min-h-0 gap-4 flex-[10]"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center items-center gap-4">
|
<h3 class="text-2xl font-bold">未完成的知會事項與代辦事項</h3>
|
||||||
<p class="text-sm">其他人數</p>
|
<!-- 可捲動內容區(水平) -->
|
||||||
<span>
|
<div class="flex flex-col gap-4 mb-6 flex-1 min-h-0">
|
||||||
<svg
|
<div class="w-full flex-1 min-h-0 overflow-x-auto overflow-y-auto">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<table class="table w-full whitespace-nowrap">
|
||||||
width="16"
|
<thead
|
||||||
height="16"
|
class="bg-brand-gray-lighter text-brand-black sticky top-0 z-[1]"
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
>
|
||||||
<path
|
<tr>
|
||||||
fill="currentColor"
|
<th class="px-3 py-2 text-left">住民</th>
|
||||||
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"
|
<th class="px-3 py-2 text-left">類型</th>
|
||||||
/>
|
<th class="px-3 py-2 text-left">表單</th>
|
||||||
</svg>
|
<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>
|
</span>
|
||||||
</div>
|
<div class="inline-flex items-center gap-2">
|
||||||
<div class="flex justify-center items-baseline gap-1">
|
<button
|
||||||
<p class="text-[36px] font-nats">36/49</p>
|
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"
|
||||||
<p class="text-[12px]">人</p>
|
:disabled="currentPage === 1"
|
||||||
</div>
|
@click="currentPage--"
|
||||||
</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>
|
</button>
|
||||||
<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 -->
|
<span class="px-2 text-sm tabular-nums text-brand-purple-dark">
|
||||||
<div class="flex-[5] grid grid-cols-2 gap-2 text-gray-300">
|
{{ currentPage }} / {{ totalPages }}
|
||||||
<div
|
</span>
|
||||||
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 -->
|
<button
|
||||||
<div
|
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"
|
||||||
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]"
|
:disabled="currentPage === totalPages"
|
||||||
|
@click="currentPage++"
|
||||||
>
|
>
|
||||||
<p>暫無內容</p>
|
下一頁
|
||||||
<!-- <h4 class="text-xl text-gray-600 font-bold">與去年同期比較</h4>
|
</button>
|
||||||
<div ref="chartCRef" class="w-[90%] h-[90%]"></div> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue";
|
||||||
import * as echarts from "echarts";
|
import * as echarts from "echarts";
|
||||||
import { brand } from "@/styles/palette";
|
import { brand } from "@/styles/palette";
|
||||||
|
// 你原本的 import 改成也帶入 watch
|
||||||
|
|
||||||
// ---- DOM Refs ----
|
/* =========================
|
||||||
const chartARef = ref(null);
|
未完成的知會事項與代辦事項
|
||||||
const chartBRef = ref(null);
|
- 10 筆/頁
|
||||||
const chartCRef = ref(null);
|
- 分頁邏輯+邊界保護
|
||||||
|
- 假資料欄位與表頭一致
|
||||||
|
========================= */
|
||||||
|
|
||||||
// ---- Chart instances ----
|
// 預設每頁 10 筆
|
||||||
let chartA, chartB, chartC;
|
const pageSize = ref(10);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
|
||||||
// 假資料工具
|
// 假資料(實務上你可以改用 API 回來的陣列)
|
||||||
function genTrend(len, start = 50, drift = 0.6, noise = 20) {
|
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 = [];
|
const arr = [];
|
||||||
let v = start;
|
let n = start;
|
||||||
for (let i = 0; i < len; i++) {
|
while (arr.length < count) {
|
||||||
v = v + drift + (Math.random() * noise * 2 - noise);
|
if (!String(n).includes("4")) arr.push(n);
|
||||||
arr.push(Number(v.toFixed(1)));
|
n++;
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commonGrid = {
|
/* ===== 假資料:住民 ===== */
|
||||||
left: 40,
|
const bedsResidents = generateBeds(12, 1001);
|
||||||
right: 20,
|
const residentsAll = [
|
||||||
top: 50,
|
{
|
||||||
bottom: 30,
|
bed: bedsResidents[0],
|
||||||
containLabel: true,
|
name: "王小明",
|
||||||
};
|
gender: "男",
|
||||||
const commonTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
|
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:三個月趨勢
|
const residentsA = residentsAll.filter((r) => r.area === "A");
|
||||||
function buildOptionA() {
|
const residentsB = residentsAll.filter((r) => r.area === "B");
|
||||||
const x = Array.from({ length: 8 }, (_, i) => `週${i + 1}`);
|
|
||||||
const legends = ["6月", "7月", "8月"];
|
|
||||||
const series = legends.map((label, idx) => ({
|
|
||||||
name: label,
|
|
||||||
type: "bar",
|
|
||||||
emphasis: { focus: "series" },
|
|
||||||
data: genTrend(x.length, 40 + idx * 2, 0.5 + idx * 0.1, 1.5),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
/* ===== 假資料:住院 ===== */
|
||||||
legend: { top: 10 },
|
const bedsInpatient = generateBeds(4, 1020);
|
||||||
tooltip: commonTooltip,
|
const inpatients = [
|
||||||
grid: commonGrid,
|
{
|
||||||
xAxis: { type: "category", data: x },
|
bed: bedsInpatient[0],
|
||||||
yAxis: { type: "value" },
|
name: "劉書豪",
|
||||||
// 指定顏色
|
hospitalDept: "台大醫院/心臟內科",
|
||||||
color: [brand.green, brand.greenLight, brand.yellow],
|
recordNo: "A1023001",
|
||||||
series,
|
status: "加護中",
|
||||||
};
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[1],
|
||||||
|
name: "高雅筑",
|
||||||
|
hospitalDept: "榮總/新陳代謝科",
|
||||||
|
recordNo: "B1023007",
|
||||||
|
status: "住院觀察",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[2],
|
||||||
|
name: "方志明",
|
||||||
|
hospitalDept: "長庚/骨科",
|
||||||
|
recordNo: "C1023011",
|
||||||
|
status: "術後恢復",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[3],
|
||||||
|
name: "鄭于庭",
|
||||||
|
hospitalDept: "國泰/神經內科",
|
||||||
|
recordNo: "D1023020",
|
||||||
|
status: "檢查中",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ESC 關閉 */
|
||||||
|
function onKeydown(e) {
|
||||||
|
if (e.key === "Escape") closeModal();
|
||||||
}
|
}
|
||||||
|
onMounted(() => window.addEventListener("keydown", onKeydown));
|
||||||
// B:六個月趨勢
|
onUnmounted(() => window.removeEventListener("keydown", onKeydown));
|
||||||
function buildOptionB() {
|
|
||||||
const x = Array.from({ length: 8 }, (_, i) => `週${i + 1}`);
|
|
||||||
const legends = ["3月", "4月", "5月", "6月", "7月", "8月"];
|
|
||||||
const series = legends.map((label, idx) => ({
|
|
||||||
name: label,
|
|
||||||
type: "bar",
|
|
||||||
emphasis: { focus: "series" },
|
|
||||||
data: genTrend(x.length, 35 + idx * 1.5, 0.4 + idx * 0.06, 1.6),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
legend: { type: "scroll", top: 10 },
|
|
||||||
tooltip: commonTooltip,
|
|
||||||
grid: commonGrid,
|
|
||||||
xAxis: { type: "category", data: x },
|
|
||||||
yAxis: { type: "value" },
|
|
||||||
color: [
|
|
||||||
brand.green,
|
|
||||||
brand.greenLight,
|
|
||||||
brand.yellow,
|
|
||||||
brand.purpleLight,
|
|
||||||
brand.purple,
|
|
||||||
brand.gray,
|
|
||||||
],
|
|
||||||
series,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// C:與去年同期比較
|
|
||||||
function buildOptionC() {
|
|
||||||
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}月`);
|
|
||||||
const lastYear = genTrend(12, 48, 0.3, 1.8);
|
|
||||||
const thisYear = lastYear.map((v, i) =>
|
|
||||||
Number((v + 1 + Math.sin(i / 2)).toFixed(1))
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
legend: { top: 10, data: ["去年", "今年"] },
|
|
||||||
tooltip: { trigger: "axis" },
|
|
||||||
grid: commonGrid,
|
|
||||||
xAxis: { type: "category", data: months, boundaryGap: false },
|
|
||||||
yAxis: { type: "value" },
|
|
||||||
color: [brand.green, brand.purple],
|
|
||||||
series: [
|
|
||||||
{ name: "去年", type: "line", smooth: true, data: lastYear },
|
|
||||||
{ name: "今年", type: "line", smooth: true, data: thisYear },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化 & resize
|
|
||||||
function initCharts() {
|
|
||||||
if (chartARef.value && !chartA) {
|
|
||||||
chartA = echarts.init(chartARef.value);
|
|
||||||
chartA.setOption(buildOptionA());
|
|
||||||
}
|
|
||||||
if (chartBRef.value && !chartB) {
|
|
||||||
chartB = echarts.init(chartBRef.value);
|
|
||||||
chartB.setOption(buildOptionB());
|
|
||||||
}
|
|
||||||
if (chartCRef.value && !chartC) {
|
|
||||||
chartC = echarts.init(chartCRef.value);
|
|
||||||
chartC.setOption(buildOptionC());
|
|
||||||
}
|
|
||||||
handleResize();
|
|
||||||
}
|
|
||||||
|
|
||||||
let resizeObserver;
|
|
||||||
function handleResize() {
|
|
||||||
const resize = () => {
|
|
||||||
chartA && chartA.resize();
|
|
||||||
chartB && chartB.resize();
|
|
||||||
chartC && chartC.resize();
|
|
||||||
};
|
|
||||||
window.addEventListener("resize", resize);
|
|
||||||
|
|
||||||
const rootEl = document.querySelector("section.flex.flex-col");
|
|
||||||
if (rootEl) {
|
|
||||||
resizeObserver = new ResizeObserver(resize);
|
|
||||||
resizeObserver.observe(rootEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function disposeCharts() {
|
|
||||||
resizeObserver && resizeObserver.disconnect();
|
|
||||||
window.removeEventListener("resize", handleResize);
|
|
||||||
chartA && chartA.dispose();
|
|
||||||
chartB && chartB.dispose();
|
|
||||||
chartC && chartC.dispose();
|
|
||||||
chartA = chartB = chartC = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await nextTick();
|
|
||||||
initCharts();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
disposeCharts();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -3,14 +3,14 @@
|
|||||||
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden"
|
class="flex flex-col gap-2 h-[calc(100vh-72px-32px)] overflow-hidden"
|
||||||
>
|
>
|
||||||
<!-- 高度比重:2 -->
|
<!-- 高度比重:2 -->
|
||||||
<div class="flex-[2] grid grid-cols-4 gap-2">
|
<div class="flex-[2] grid grid-cols-3 gap-2">
|
||||||
<!-- 住民人數 Card -->
|
<!-- 住民人數 Card -->
|
||||||
<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"
|
||||||
@click="openModal('residents')"
|
@click="openModal('residents')"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center items-center gap-4">
|
<div class="flex justify-center items-center gap-4">
|
||||||
<p class="text-sm">住民人數</p>
|
<p class="text-sm">現在住民/立案床數</p>
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -37,7 +37,7 @@
|
|||||||
@click="openModal('inpatients')"
|
@click="openModal('inpatients')"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center items-center gap-4">
|
<div class="flex justify-center items-center gap-4">
|
||||||
<p class="text-sm">住院人數</p>
|
<p class="text-sm">今日住院/當月累積住院</p>
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -61,7 +61,7 @@
|
|||||||
<!-- ===== Modal(放在同一個 <template> 中,建議貼在最底部)===== -->
|
<!-- ===== Modal(放在同一個 <template> 中,建議貼在最底部)===== -->
|
||||||
<div
|
<div
|
||||||
v-if="showModal"
|
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"
|
@keydown.esc="closeModal"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
@ -95,7 +95,7 @@
|
|||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full text-sm text-brand-black">
|
<table class="min-w-full text-sm text-brand-black">
|
||||||
<thead>
|
<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>
|
<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
|
<tr
|
||||||
v-for="r in residentsA"
|
v-for="r in residentsA"
|
||||||
:key="r.bed"
|
: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 font-mono">{{ r.bed }}</td>
|
||||||
<td class="px-3 py-2">{{ r.name }}</td>
|
<td class="px-3 py-2">{{ r.name }}</td>
|
||||||
@ -127,7 +127,7 @@
|
|||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full text-sm text-brand-black">
|
<table class="min-w-full text-sm text-brand-black">
|
||||||
<thead>
|
<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>
|
<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
|
<tr
|
||||||
v-for="r in residentsB"
|
v-for="r in residentsB"
|
||||||
:key="r.bed"
|
: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 font-mono">{{ r.bed }}</td>
|
||||||
<td class="px-3 py-2">{{ r.name }}</td>
|
<td class="px-3 py-2">{{ r.name }}</td>
|
||||||
@ -157,7 +157,6 @@
|
|||||||
|
|
||||||
<!-- body:住院資訊 -->
|
<!-- body:住院資訊 -->
|
||||||
<div v-else-if="modalType === 'inpatients'">
|
<div v-else-if="modalType === 'inpatients'">
|
||||||
<h4 class="text-lg font-semibold text-gray-700 mb-3">清單</h4>
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full text-sm text-brand-black">
|
<table class="min-w-full text-sm text-brand-black">
|
||||||
<thead>
|
<thead>
|
||||||
@ -189,7 +188,7 @@
|
|||||||
<!-- footer -->
|
<!-- footer -->
|
||||||
<div class="mt-6 text-right">
|
<div class="mt-6 text-right">
|
||||||
<button
|
<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"
|
@click="closeModal"
|
||||||
>
|
>
|
||||||
關閉
|
關閉
|
||||||
@ -202,31 +201,7 @@
|
|||||||
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">
|
<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"
|
|
||||||
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>
|
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -250,26 +225,26 @@
|
|||||||
|
|
||||||
<!-- 高度比重:5 -->
|
<!-- 高度比重:5 -->
|
||||||
<div class="flex-[5] grid grid-cols-2 gap-2">
|
<div class="flex-[5] grid grid-cols-2 gap-2">
|
||||||
|
<!-- 佔床率 -->
|
||||||
<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="chartARef" class="w-full h-[90%] min-h-[200px]"></div>
|
||||||
<div ref="chartARef" class="w-full h-full"></div>
|
|
||||||
</div>
|
</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-[90%] min-h-[200px]"></div>
|
||||||
<div ref="chartBRef" class="w-full h-full"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 高度比重:5 -->
|
<!-- 高度比重:5 -->
|
||||||
<div
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@ -298,6 +273,89 @@ function genTrend(len, start = 50, drift = 0.6, noise = 20) {
|
|||||||
return arr;
|
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 = {
|
const commonGrid = {
|
||||||
left: 40,
|
left: 40,
|
||||||
right: 20,
|
right: 20,
|
||||||
@ -307,87 +365,241 @@ const commonGrid = {
|
|||||||
};
|
};
|
||||||
const commonTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
|
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() {
|
function buildOptionA() {
|
||||||
const x = Array.from({ length: 8 }, (_, i) => `op${i + 1}`);
|
const labels = getLastNMonthLabels(6); // 近六個月
|
||||||
const legends = ["6月", "7月", "8月"];
|
// 產生六組樣本(每組 30 筆、約 80~92%),並限制在 0~100 之間
|
||||||
const series = legends.map((label, idx) => ({
|
const raw = labels.map((_, i) =>
|
||||||
name: label,
|
genRates(30, 88 - i * 1.5, 10).map((v) => Math.max(0, Math.min(100, v)))
|
||||||
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 { boxData, outliers } = toBoxplot(raw);
|
||||||
|
const BOX = brand.purpleDark;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
legend: { top: 10, data: ["去年", "今年"] },
|
tooltip: {
|
||||||
tooltip: { trigger: "axis" },
|
trigger: "item",
|
||||||
grid: commonGrid,
|
formatter: (p) => {
|
||||||
xAxis: { type: "category", data: months, boundaryGap: false },
|
if (p.seriesType === "boxplot") {
|
||||||
yAxis: { type: "value" },
|
const [min, med, max] = p.data;
|
||||||
color: [brand.purple, brand.green],
|
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: [
|
series: [
|
||||||
{
|
{
|
||||||
name: "去年",
|
name: "箱型圖",
|
||||||
type: "line",
|
type: "boxplot",
|
||||||
smooth: true,
|
data: boxData,
|
||||||
data: lastYear,
|
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
|
// 初始化 & resize
|
||||||
function initCharts() {
|
function initCharts() {
|
||||||
if (chartARef.value && !chartA) {
|
if (chartARef.value && !chartA) {
|
||||||
|
@ -13,5 +13,7 @@ export const brand = {
|
|||||||
black: "#424242",
|
black: "#424242",
|
||||||
gray: "#828282",
|
gray: "#828282",
|
||||||
grayLight: "#E9E9E9",
|
grayLight: "#E9E9E9",
|
||||||
|
grayLighter: "#F6F6F6",
|
||||||
grayDark: "#D2D2D2",
|
grayDark: "#D2D2D2",
|
||||||
|
white: "#ffffff"
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ module.exports = {
|
|||||||
"2xl": "2.5rem",
|
"2xl": "2.5rem",
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
// 讓 container 在各斷點對齊你自訂的寬度
|
// 讓 container 在各斷點對齊自訂的寬度
|
||||||
sm: "640px",
|
sm: "640px",
|
||||||
md: "768px",
|
md: "768px",
|
||||||
lg: "1024px",
|
lg: "1024px",
|
||||||
@ -33,14 +33,14 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
// 讓你能寫 class="font-noto"
|
// class="font-noto"
|
||||||
noto: ["Noto Sans TC", "sans-serif"],
|
noto: ["Noto Sans TC", "sans-serif"],
|
||||||
// 讓你能寫 class="font-nats"
|
// class="font-nats"
|
||||||
nats: ["NATS-Regular", "sans-serif"],
|
nats: ["NATS-Regular", "sans-serif"],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
brand: {
|
brand: {
|
||||||
green: { DEFAULT: "#34D5C8", light: "#C4FBE5", dark:"#0CA99C" },
|
green: { DEFAULT: "#34D5C8", light: "#C4FBE5", dark: "#0CA99C" },
|
||||||
red: "#FF8678",
|
red: "#FF8678",
|
||||||
purple: {
|
purple: {
|
||||||
DEFAULT: "#A5BEFF",
|
DEFAULT: "#A5BEFF",
|
||||||
@ -54,6 +54,7 @@ module.exports = {
|
|||||||
black: "#424242",
|
black: "#424242",
|
||||||
gray: {
|
gray: {
|
||||||
DEFAULT: "#828282",
|
DEFAULT: "#828282",
|
||||||
|
lighter: "#EEEEEE",
|
||||||
light: "#E9E9E9",
|
light: "#E9E9E9",
|
||||||
dark: "#D2D2D2",
|
dark: "#D2D2D2",
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user