feat: 製作 demo 用 modals
This commit is contained in:
parent
a6beaae3e3
commit
c4d78bd5b6
21
public/img/hotSpot.svg
Normal file
21
public/img/hotSpot.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<svg width="164" height="164" viewBox="0 0 164 164" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<circle cx="81.7363" cy="81.8212" r="40" stroke-width="60" style="stroke:grey" />
|
||||||
|
</g>
|
||||||
|
<g filter="url(#filter0_d)">
|
||||||
|
<circle cx="81.7363" cy="81.8212" r="20" stroke="white" stroke-width="60"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d" x="0.174763" y="0.259602" width="163.123" height="163.123" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||||
|
<feOffset/>
|
||||||
|
<feGaussianBlur stdDeviation="5.61135"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 938 B |
@ -38,8 +38,6 @@ const FLOOR_DBIDS = { "9F": 14973, "8F": 14262 };
|
|||||||
// ====== 顏色 / Sprite數量(可調) ======
|
// ====== 顏色 / Sprite數量(可調) ======
|
||||||
const BRAND_GREEN = "#34D5C8";
|
const BRAND_GREEN = "#34D5C8";
|
||||||
const BRAND_RED = "#FF8678";
|
const BRAND_RED = "#FF8678";
|
||||||
const SPRITES_PER_FLOOR = 6;
|
|
||||||
const RED_PER_FLOOR = 2;
|
|
||||||
|
|
||||||
// (可調)本地 SVF 路徑(相對於 public)
|
// (可調)本地 SVF 路徑(相對於 public)
|
||||||
const MODEL_SVF_PATH = `/upload/forge/0.svf`;
|
const MODEL_SVF_PATH = `/upload/forge/0.svf`;
|
||||||
@ -50,6 +48,37 @@ let viewer = null;
|
|||||||
const viewerReady = ref(false);
|
const viewerReady = ref(false);
|
||||||
const activeFloor = ref("9F"); // 預設 9F
|
const activeFloor = ref("9F"); // 預設 9F
|
||||||
|
|
||||||
|
// ---- Resident Modal 狀態 ----
|
||||||
|
const modalOpen = ref(false);
|
||||||
|
const modalData = ref(null);
|
||||||
|
|
||||||
|
function buildResidentModalData(d) {
|
||||||
|
const vacant = d?.state === "offnormal" && !d?.special; // 空床
|
||||||
|
return {
|
||||||
|
name: vacant ? "-" : d?.residentsName ?? "-",
|
||||||
|
sex: vacant ? "-" : d?.residentsSex ?? "-",
|
||||||
|
age: vacant ? "-" : d?.residentsAge ?? "-",
|
||||||
|
startTime: vacant ? "-" : d?.startTime ?? "-",
|
||||||
|
healthStatus: vacant ? "-" : d?.healthStatus ?? "一般",
|
||||||
|
medicationStatus: vacant ? "-" : d?.medicationStatus ?? "規律服藥",
|
||||||
|
specialEvent: d?.special
|
||||||
|
? d?.specialEvent ?? "住院中"
|
||||||
|
: d?.specialEvent ?? "-",
|
||||||
|
note: vacant ? "-" : d?.note ?? "-",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function openResidentModal(L, e) {
|
||||||
|
e?.stopPropagation?.(); // 避免事件冒泡
|
||||||
|
bringToFrontById(L.id);
|
||||||
|
modalData.value = buildResidentModalData(L.data);
|
||||||
|
modalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeResidentModal() {
|
||||||
|
modalOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
// 嘗試從 ?floor=8F|9F 或 localStorage 還原
|
// 嘗試從 ?floor=8F|9F 或 localStorage 還原
|
||||||
function resolveInitialFloor() {
|
function resolveInitialFloor() {
|
||||||
const q = (route.query?.floor || "").toString().toUpperCase();
|
const q = (route.query?.floor || "").toString().toUpperCase();
|
||||||
@ -132,27 +161,6 @@ function getNodeWorldBounds(model, dbId) {
|
|||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以 3x2 固定網格分佈 6 個點
|
|
||||||
function gridPositions(box, count = 6) {
|
|
||||||
const THREE = getThree();
|
|
||||||
const xs = [0.2, 0.5, 0.8];
|
|
||||||
const ys = [0.3, 0.7];
|
|
||||||
const res = [];
|
|
||||||
const min = box.min,
|
|
||||||
max = box.max;
|
|
||||||
const w = max.x - min.x,
|
|
||||||
h = max.y - min.y,
|
|
||||||
zH = max.z - min.z;
|
|
||||||
const z = max.z + Math.max(0.2, zH * 0.05);
|
|
||||||
for (let r = 0; r < ys.length; r++) {
|
|
||||||
for (let c = 0; c < xs.length; c++) {
|
|
||||||
if (res.length >= count) break;
|
|
||||||
res.push(new THREE.Vector3(min.x + xs[c] * w, min.y + ys[r] * h, z));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 把 [0~1] 比例座標轉成實際 3D 位置(固定佈局用)
|
// 把 [0~1] 比例座標轉成實際 3D 位置(固定佈局用)
|
||||||
function positionsFromRatios(box, ratios, zLiftRatio = 0.05) {
|
function positionsFromRatios(box, ratios, zLiftRatio = 0.05) {
|
||||||
const THREE = getThree();
|
const THREE = getThree();
|
||||||
@ -242,6 +250,15 @@ async function rebuildSpritesForFloor(floorKey) {
|
|||||||
residentsSex: pick(SEX_POOL, spriteDbId + 1),
|
residentsSex: pick(SEX_POOL, spriteDbId + 1),
|
||||||
residentsAge: ageFromSeed(spriteDbId + 2),
|
residentsAge: ageFromSeed(spriteDbId + 2),
|
||||||
startTime: startDateFromSeed(spriteDbId + 3),
|
startTime: startDateFromSeed(spriteDbId + 3),
|
||||||
|
|
||||||
|
avatarUrl: Math.random() > 0.4 ? pick(AVATAR_POOL, spriteDbId + 4) : null,
|
||||||
|
healthStatus: pick(HEALTH_POOL, spriteDbId + 5),
|
||||||
|
medicationStatus: pick(MEDICATION_POOL, spriteDbId + 6),
|
||||||
|
specialEvent:
|
||||||
|
isRed && i === poses.length - 2
|
||||||
|
? "住院中"
|
||||||
|
: pick(SPECIAL_POOL, spriteDbId + 7),
|
||||||
|
note: pick(NOTE_POOL, spriteDbId + 8),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -296,6 +313,7 @@ function updateLabelPositions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attachCameraEventsForLabels() {
|
function attachCameraEventsForLabels() {
|
||||||
|
camEvtCleanup?.(); // 先清掉舊的
|
||||||
const onUpdate = () => requestAnimationFrame(updateLabelPositions);
|
const onUpdate = () => requestAnimationFrame(updateLabelPositions);
|
||||||
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, onUpdate);
|
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, onUpdate);
|
||||||
viewer.addEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, onUpdate);
|
viewer.addEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, onUpdate);
|
||||||
@ -342,8 +360,7 @@ async function onClickFloor(next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 強制首頁預設 9F(避免舊的 localStorage/路由殘值影響)
|
activeFloor.value = resolveInitialFloor();
|
||||||
activeFloor.value = "9F";
|
|
||||||
|
|
||||||
await initViewer(forgeDom.value);
|
await initViewer(forgeDom.value);
|
||||||
const model = await loadModel(MODEL_SVF_PATH);
|
const model = await loadModel(MODEL_SVF_PATH);
|
||||||
@ -406,7 +423,21 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const zones = ["全部", "VIP", "換管", "失能", "失智"];
|
const zones = ["VIP", "換管", "失能", "失智"];
|
||||||
|
|
||||||
|
// 用 Set 做好操作與效能
|
||||||
|
const selectedZones = ref(new Set());
|
||||||
|
|
||||||
|
// 切換某個分區的選取狀態
|
||||||
|
const toggleZone = (zone) => {
|
||||||
|
const set = new Set(selectedZones.value);
|
||||||
|
if (set.has(zone)) set.delete(zone);
|
||||||
|
else set.add(zone);
|
||||||
|
selectedZones.value = set;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isZoneSelected = (zone) => selectedZones.value.has(zone);
|
||||||
|
|
||||||
const activeZone = ref("全部");
|
const activeZone = ref("全部");
|
||||||
const onClickZone = (zone) => {
|
const onClickZone = (zone) => {
|
||||||
activeZone.value = zone;
|
activeZone.value = zone;
|
||||||
@ -455,6 +486,19 @@ const NAME_POOL = [
|
|||||||
"郭怡君",
|
"郭怡君",
|
||||||
"洪嘉文",
|
"洪嘉文",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const AVATAR_POOL = [
|
||||||
|
"/img/avatars/m1.png",
|
||||||
|
"/img/avatars/m2.png",
|
||||||
|
"/img/avatars/f1.png",
|
||||||
|
"/img/avatars/f2.png",
|
||||||
|
]; // 可自行準備,若沒有檔案也沒關係,模板有 fallback
|
||||||
|
|
||||||
|
const HEALTH_POOL = ["一般", "良好", "需觀察", "虛弱"];
|
||||||
|
const MEDICATION_POOL = ["規律服藥", "偶爾忘記", "暫停服藥", "需家屬協助"];
|
||||||
|
const SPECIAL_POOL = ["住院中", "跌倒觀察", "復健中", "門診追蹤"];
|
||||||
|
const NOTE_POOL = ["-", "對花生過敏", "家屬每週三探視", "夜間需加護理巡房"];
|
||||||
|
|
||||||
const SEX_POOL = ["男", "女"];
|
const SEX_POOL = ["男", "女"];
|
||||||
function seededRand(seed) {
|
function seededRand(seed) {
|
||||||
const s = Math.sin(seed * 9301 + 49297) * 233280;
|
const s = Math.sin(seed * 9301 + 49297) * 233280;
|
||||||
@ -475,37 +519,51 @@ function startDateFromSeed(seed) {
|
|||||||
return `${d.getFullYear()}/${mm}/${dd}`;
|
return `${d.getFullYear()}/${mm}/${dd}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 選單選項(可自行增減)
|
// 選單選項
|
||||||
const infoOptions = [
|
const infoOptions = [
|
||||||
{ label: "無顯示", value: "none" },
|
{ label: "無顯示", value: "none" },
|
||||||
{ label: "有住民", value: "occupied" },
|
{ label: "有住民", value: "occupied" },
|
||||||
{ label: "空床", value: "vacant" },
|
{ label: "空床", value: "vacant" },
|
||||||
{ label: "住院中", value: "hospitalized" },
|
{ label: "住院中", value: "hospitalized" },
|
||||||
|
{ label: "請假中", value: "leave" }, // ← 新增
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredLabels = computed(() => {
|
const filteredLabels = computed(() => {
|
||||||
// 單獨顯示模式:只回傳被點擊的那一顆
|
// 單獨顯示模式(保留你原本的行為)
|
||||||
if (soloSpriteId.value != null) {
|
if (soloSpriteId.value != null) {
|
||||||
|
if (selectedInfo.value === "none" || selectedInfo.value === "leave")
|
||||||
|
return [];
|
||||||
return labels.value.filter((L) => L.id === soloSpriteId.value);
|
return labels.value.filter((L) => L.id === soloSpriteId.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 以下為全域篩選(依 infoOptions + 分區)----
|
// none / leave = 全隱(依你前面需求)
|
||||||
const info = selectedInfo.value; // none / occupied / vacant / hospitalized
|
if (selectedInfo.value === "none" || selectedInfo.value === "leave") {
|
||||||
const zone = activeZone.value; // 全部 / VIP / 換管 / 失能 / 失智(若你有真的 zone 欄位可改用)
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = selectedInfo.value; // 'occupied' | 'vacant' | 'hospitalized'
|
||||||
|
const selectedSet = selectedZones.value; // Set
|
||||||
|
|
||||||
return labels.value.filter((L) => {
|
return labels.value.filter((L) => {
|
||||||
// 分區(示例:用名稱前綴 "A 區 ...", 若你已在資料有 L.data.zone,改成 `zone === '全部' || L.data.zone === zone`)
|
// ===== 分區多選判斷 =====
|
||||||
const inZone = zone === "全部" || L.data.name?.startsWith(`${zone} 區`);
|
// 有選任何分區 → 必須命中其中之一
|
||||||
|
// 沒選任何分區 → 不過濾(視為全部)
|
||||||
|
const inZone =
|
||||||
|
selectedSet.size === 0
|
||||||
|
? true
|
||||||
|
: L.data?.zone
|
||||||
|
? selectedSet.has(L.data.zone)
|
||||||
|
: true;
|
||||||
|
|
||||||
|
// ===== 原本的狀態判斷(略調整)=====
|
||||||
const isRed = L.data.state === "offnormal";
|
const isRed = L.data.state === "offnormal";
|
||||||
const isHosp = isRed && !!L.data.special; // 住院中:紅 + special
|
const isHosp = isRed && !!L.data.special; // 住院中:紅 + special
|
||||||
const isGreen = !isRed && !L.data.special; // 有住民:綠(依你資料邏輯)
|
const isGreen = !isRed && !L.data.special; // 有住民:綠
|
||||||
|
|
||||||
let stateOk = true;
|
let stateOk = true;
|
||||||
if (info === "none") stateOk = false; // 無顯示 → 全隱(配合需求 2)
|
if (info === "occupied") stateOk = isGreen;
|
||||||
if (info === "occupied") stateOk = isGreen; // 有住民 → 綠
|
if (info === "vacant") stateOk = isRed; // 空床 = 只要是紅色,都包含(含住院)
|
||||||
if (info === "vacant") stateOk = isRed; // 空床 → 紅(無論 special 與否)
|
if (info === "hospitalized") stateOk = isHosp;
|
||||||
if (info === "hospitalized") stateOk = isHosp; // 住院中 → 紅+special
|
|
||||||
|
|
||||||
return inZone && stateOk;
|
return inZone && stateOk;
|
||||||
});
|
});
|
||||||
@ -588,8 +646,21 @@ onUnmounted(() => {
|
|||||||
@click="bringToFrontById(L.id)"
|
@click="bringToFrontById(L.id)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto bg-white/95 border rounded-md shadow px-2 py-1 text-xs"
|
class="pointer-events-auto relative bg-white/95 border border-gray-300 rounded-lg 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">
|
<ul class="list-none">
|
||||||
<!-- 第一行:床號永遠顯示 -->
|
<!-- 第一行:床號永遠顯示 -->
|
||||||
<li class="flex justify-between items-center gap-1">
|
<li class="flex justify-between items-center gap-1">
|
||||||
@ -623,8 +694,8 @@ onUnmounted(() => {
|
|||||||
<!-- 床號:永遠顯示 -->
|
<!-- 床號:永遠顯示 -->
|
||||||
<span class="align-middle">{{ L.data.name }}</span>
|
<span class="align-middle">{{ L.data.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 眼睛 icon -->
|
||||||
<div class="text-gray-400 hover:text-gray-600">
|
<div class="text-gray-400" title="查看詳細">
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -687,96 +758,88 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 過濾 popover 顯示什麼狀態-->
|
<!-- 分區切換(多選|預設不選=顯示全部) -->
|
||||||
<div
|
|
||||||
class="absolute top-4 right-4 text-sm flex justify-center items-center gap-2 z-50"
|
|
||||||
>
|
|
||||||
<div class="relative">
|
|
||||||
<div
|
|
||||||
ref="infoTriggerRef"
|
|
||||||
class="btn text-brand-purple-dark bg-brand-purple-light hover:opacity-90 shadow-md border-none rounded-full px-6 tracking-widest cursor-pointer"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-haspopup="true"
|
|
||||||
:aria-expanded="infoOpen ? 'true' : 'false'"
|
|
||||||
@click="toggleInfo"
|
|
||||||
@keydown.enter.prevent="toggleInfo"
|
|
||||||
@keydown.space.prevent="toggleInfo"
|
|
||||||
>
|
|
||||||
{{ infoDisplay }}
|
|
||||||
<span class="ml-2 inline-block align-middle">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="m98 190.06l139.78 163.12a24 24 0 0 0 36.44 0L414 190.06c13.34-15.57 2.28-39.62-18.22-39.62h-279.6c-20.5 0-31.56 24.05-18.18 39.62"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 下拉選單(使用 <ul> 與 <li>) -->
|
|
||||||
<div
|
|
||||||
v-show="infoOpen"
|
|
||||||
ref="infoPanelRef"
|
|
||||||
class="absolute top-12 right-0 z-50 w-48 rounded-xl border border-gray-100 bg-white shadow-lg p-2"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<ul class="max-h-48 overflow-y-auto">
|
|
||||||
<li
|
|
||||||
v-for="opt in infoOptions"
|
|
||||||
:key="opt.value"
|
|
||||||
@click="selectInfo(opt)"
|
|
||||||
class="px-3 py-2 rounded-md cursor-pointer hover:bg-gray-100"
|
|
||||||
:class="selectedInfo === opt.value ? 'bg-brand-green-light' : ''"
|
|
||||||
>
|
|
||||||
{{ opt.label }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分區切換 -->
|
|
||||||
<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"
|
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>
|
<p class="ps-4 py-2">分區|</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-for="zone in zones"
|
v-for="zone in zones"
|
||||||
:key="zone"
|
:key="zone"
|
||||||
class="px-4 py-2 rounded-md hover:bg-gray-100"
|
class="px-4 py-2 rounded-md hover:bg-gray-100"
|
||||||
:class="
|
:class="
|
||||||
activeZone === zone
|
isZoneSelected(zone)
|
||||||
? 'bg-brand-green-light bg-opacity-50'
|
? 'bg-brand-green-light bg-opacity-50'
|
||||||
: 'bg-white'
|
: 'bg-white'
|
||||||
"
|
"
|
||||||
@click="onClickZone(zone)"
|
@click="toggleZone(zone)"
|
||||||
:aria-pressed="activeZone === zone"
|
:aria-pressed="isZoneSelected(zone)"
|
||||||
>
|
>
|
||||||
{{ zone }}
|
{{ zone }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 圖例 -->
|
<!-- 床位資訊-->
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-4 left-4 text-sm flex justify-center gap-2 items-center z-10 bg-white border rounded-md shadow px-4"
|
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>
|
<p class="py-2">床位資訊|</p>
|
||||||
<div class="flex justify-start items-center gap-4">
|
|
||||||
<div class="flex justify-start items-center gap-2">
|
<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 class="w-2 h-2 bg-brand-green rounded-full inline-block"></span>
|
||||||
<p>有住民</p>
|
<span>有住民</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="flex justify-start items-center gap-2">
|
|
||||||
|
<!-- 空床(紅) -->
|
||||||
|
<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 class="w-2 h-2 bg-brand-red rounded-full inline-block"></span>
|
||||||
<p>空床</p>
|
<span>空床</span>
|
||||||
</div>
|
</button>
|
||||||
<div class="flex justify-start items-center gap-2">
|
|
||||||
|
<!-- 住院中(紅 + 黃色警示) -->
|
||||||
|
<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">
|
<span class="w-2 h-2 text-brand-yellow-dark inline-block">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -790,11 +853,91 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<p>住院中</p>
|
<span>住院中</span>
|
||||||
</div>
|
</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>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -1,282 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import {
|
|
||||||
ref,
|
|
||||||
onMounted,
|
|
||||||
defineProps,
|
|
||||||
computed,
|
|
||||||
onUnmounted,
|
|
||||||
watch,
|
|
||||||
provide,
|
|
||||||
} from "vue";
|
|
||||||
import { getUrn, getAccessToken } from "@/apis/forge";
|
|
||||||
// import { twMerge } from "tailwind-merge";
|
|
||||||
// import hexToRgb from "@/util/hexToRgb";
|
|
||||||
// import getModalPosition from "@/util/getModalPosition";
|
|
||||||
// import useSystemStatusByBaja from "@/hooks/baja/useSystemStatusByBaja";
|
|
||||||
// import ForgeInfoModal from "@/components/forge/ForgeInfoModal.vue";
|
|
||||||
// import useAlarmStore from "@/stores/useAlarmStore";
|
|
||||||
import useForgeSprite from "@/hooks/forge/useForgeSprite";
|
|
||||||
import useHeatmapBarStore from "@/stores/useHeatmapBarStore";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
initialData: Object,
|
|
||||||
cubeStyle: {
|
|
||||||
type: Object,
|
|
||||||
default: {
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const store = useHeatmapBarStore();
|
|
||||||
|
|
||||||
const { updateDataVisualization, createSprites, showSubSystemObjects, forgeClickListener, clear, setCameraPosition } = useForgeSprite()
|
|
||||||
|
|
||||||
const forgeDom = ref(null);
|
|
||||||
|
|
||||||
const initViewer = (container) => {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
// Autodesk.Viewing.Initializer({ getAccessToken }, function () {
|
|
||||||
// const config = {
|
|
||||||
// extensions: ["Autodesk.DataVisualization", "Autodesk.DocumentBrowser"],
|
|
||||||
// };
|
|
||||||
// let viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
|
|
||||||
// Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
|
|
||||||
// viewer.start();
|
|
||||||
// resolve(viewer);
|
|
||||||
// });
|
|
||||||
|
|
||||||
Autodesk.Viewing.Initializer(
|
|
||||||
{
|
|
||||||
env: "Local",
|
|
||||||
language: "en",
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
const config = {
|
|
||||||
extensions: [
|
|
||||||
"Autodesk.DataVisualization",
|
|
||||||
"Autodesk.DocumentBrowser",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
let viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
|
|
||||||
Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
|
|
||||||
viewer.start();
|
|
||||||
viewer.setGroundShadow(false);
|
|
||||||
viewer.impl.renderer().setClearAlpha(0);
|
|
||||||
viewer.impl.glrenderer().setClearColor(0xffffff, 0);
|
|
||||||
viewer.impl.invalidate(true);
|
|
||||||
resolve(viewer);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用本地 .svf 文件加載模型
|
|
||||||
const loadModel = (viewer, filePath) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
viewer.loadModel(
|
|
||||||
filePath,
|
|
||||||
{},
|
|
||||||
(model) => {
|
|
||||||
viewer.impl.invalidate(true);
|
|
||||||
viewer.fitToView();
|
|
||||||
setTimeout(() => {
|
|
||||||
setCameraPosition(
|
|
||||||
{
|
|
||||||
x: 241.40975707867645,
|
|
||||||
y: -260.4481491801548,
|
|
||||||
z: 129.5719879121458,
|
|
||||||
}, // 攝影機的新位置
|
|
||||||
{
|
|
||||||
x: -183.36302786348594,
|
|
||||||
y: 194.05657710941966,
|
|
||||||
z: -149.3902249981004,
|
|
||||||
} // 攝影機的焦點
|
|
||||||
);
|
|
||||||
}, 500);
|
|
||||||
updateDataVisualization(viewer)
|
|
||||||
resolve(model);
|
|
||||||
console.log("模型加載完成");
|
|
||||||
},
|
|
||||||
reject
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// const loadModel = (viewer) => {
|
|
||||||
// return new Promise(function (resolve, reject) {
|
|
||||||
// async function onDocumentLoadSuccess(doc) {
|
|
||||||
// console.log("模型加載完成");
|
|
||||||
// viewer.setGroundShadow(false);
|
|
||||||
// viewer.impl.renderer().setClearAlpha(0); //clear alpha channel
|
|
||||||
// viewer.impl.glrenderer().setClearColor(0xffffff, 0); //set transparent background, color code does not matter
|
|
||||||
// viewer.impl.invalidate(true); //trigger rendering
|
|
||||||
|
|
||||||
// const documentNode = await viewer.loadDocumentNode(
|
|
||||||
// doc,
|
|
||||||
// doc.getRoot().getDefaultGeometry()
|
|
||||||
// );
|
|
||||||
// updateDataVisualization(viewer)
|
|
||||||
// resolve(documentNode);
|
|
||||||
// }
|
|
||||||
// function onDocumentLoadFailure(code, message, errors) {
|
|
||||||
// reject({ code, message, errors });
|
|
||||||
// }
|
|
||||||
// Autodesk.Viewing.Document.load(
|
|
||||||
// "urn:" + urn,
|
|
||||||
// onDocumentLoadSuccess,
|
|
||||||
// onDocumentLoadFailure
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
const initForge = async () => {
|
|
||||||
// getUrn().then((res) => {
|
|
||||||
// if (!res.isSuccess) return;
|
|
||||||
|
|
||||||
// initViewer(forgeDom.value).then((viewer) => {
|
|
||||||
// loadModel(viewer, res.data[0].urn_3D).then(() => {
|
|
||||||
// viewer.addEventListener(
|
|
||||||
// Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
|
|
||||||
// async function () {
|
|
||||||
// console.log(
|
|
||||||
// "Autodesk.Viewing.GEOMETRY_LOADED_EVENT",
|
|
||||||
// viewer.isLoadDone()
|
|
||||||
// );
|
|
||||||
// // updateForgeViewer(viewer);
|
|
||||||
// createSprites()
|
|
||||||
// hideAllObjects();
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// viewer.addEventListener(
|
|
||||||
// Autodesk.Viewing.CAMERA_CHANGE_EVENT,
|
|
||||||
// function (e) {
|
|
||||||
// // viewer.isLoadDone() &&
|
|
||||||
// // updateDbidPosition(this, subscribeData.value);
|
|
||||||
// console.log(
|
|
||||||
// "camera position changed: ",
|
|
||||||
// NOP_VIEWER.navigation.getTarget(),
|
|
||||||
// e.camera.position
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
|
||||||
const viewer = await initViewer(forgeDom.value)
|
|
||||||
const filePath = `${FILE_BASEURL}/upload/forge/0.svf`;
|
|
||||||
await loadModel(viewer, filePath)
|
|
||||||
viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
|
|
||||||
async function () {
|
|
||||||
console.log(
|
|
||||||
"Autodesk.Viewing.GEOMETRY_LOADED_EVENT",
|
|
||||||
viewer.isLoadDone()
|
|
||||||
);
|
|
||||||
|
|
||||||
showSubSystemObjects();
|
|
||||||
createSprites();
|
|
||||||
forgeClickListener();
|
|
||||||
})
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
console.log("Forge 加載");
|
|
||||||
initForge();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 傳遞目前點擊資訊
|
|
||||||
const currentInfoModalData = ref(null);
|
|
||||||
const isMobile = (pointerType) => {
|
|
||||||
return pointerType !== "mouse"; // is desktop
|
|
||||||
};
|
|
||||||
const getCurrentInfoModalData = (e, position, value) => {
|
|
||||||
const mobile = isMobile(e.pointerType);
|
|
||||||
currentInfoModalData.value = {
|
|
||||||
initPos: mobile
|
|
||||||
? { left: `50%`, top: `50%` }
|
|
||||||
: { left: `${position.left}px`, top: `${position.top}px` },
|
|
||||||
value,
|
|
||||||
isMobile: mobile,
|
|
||||||
};
|
|
||||||
forge_info_modal.showModal();
|
|
||||||
};
|
|
||||||
onUnmounted(() => {
|
|
||||||
console.log("Forge 銷毀");
|
|
||||||
|
|
||||||
clear();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ForgeInfoModal :data="currentInfoModalData" />
|
|
||||||
<div id="forge-preview" ref="forgeDom" class="relative w-full h-full min-h-full">
|
|
||||||
<div v-show="store.heat_bar_isShow" class="absolute z-10 heatbar">
|
|
||||||
<div class="w-40 flex justify-between text-[10px] mb-1">
|
|
||||||
<span v-for="value in store.heatmapConfig?.range" class="text-gradient-1">{{ value }} {{
|
|
||||||
store.heatmapConfig?.unit }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-40 h-3" :style="{
|
|
||||||
background: `linear-gradient(
|
|
||||||
to right,
|
|
||||||
${store.heatmapConfig?.color[0]} 0%,
|
|
||||||
${store.heatmapConfig?.color[1]} 100%
|
|
||||||
)`
|
|
||||||
}"></div>
|
|
||||||
</div>
|
|
||||||
<!-- label -->
|
|
||||||
<!-- https://github.com/augustogoncalves/forge-plant-operation/blob/master/forgeSample/wwwroot/js/iconExtension.js -->
|
|
||||||
<!-- https://github.com/dukedhx/viewer-iot-react-feathersjs/blob/master/src/client/iconExtension.js -->
|
|
||||||
|
|
||||||
<label v-for="value in subscribeData" :key="key" :data-dbid="value.forge_dbid" :class="twMerge(
|
|
||||||
`after:border-t-[${value.currentColor}]`,
|
|
||||||
'flex items-center justify-center h-12 -translate-x-1/2 -translate-y-1/5 absolute z-50 px-5 py-4 text-center rounded-md text-lg border-2 border-white',
|
|
||||||
'after:absolute after:border-t-[10px] after:border-x-[12px] after:border-x-transparent after:-bottom-[8px] after:left-1/2 after:-translate-x-1/2 ',
|
|
||||||
'before:absolute before:border-t-[12px] before:border-x-[14px] before:border-x-transparent before:-bottom-[12px] before:left-1/2 before:-translate-x-1/2 before:border-white'
|
|
||||||
)
|
|
||||||
" :style="{
|
|
||||||
left: `${Math.floor(value.device_coordinate_3d.x)}px`,
|
|
||||||
top: `${Math.floor(value.device_coordinate_3d.y) - 100}px`,
|
|
||||||
display: value.is_show,
|
|
||||||
backgroundColor: value.currentColor,
|
|
||||||
}" @click.prevent="(e) =>
|
|
||||||
getCurrentInfoModalData(
|
|
||||||
e,
|
|
||||||
{ left: e.clientX, top: e.clientY },
|
|
||||||
value
|
|
||||||
)
|
|
||||||
">
|
|
||||||
<span class="mr-2">{{ value.full_name }}</span>
|
|
||||||
<span v-if="value.alarmMsg">{{ value.alarmMsg }}</span>
|
|
||||||
<span v-else>{{ value.show_value }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="css">
|
|
||||||
.adsk-viewing-viewer {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#guiviewer3d-toolbar {
|
|
||||||
display: none;
|
|
||||||
bottom: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewcubeWrapper {
|
|
||||||
right: v-bind("`${props.cubeStyle.right}%`") !important;
|
|
||||||
top: v-bind("`${props.cubeStyle.top}%`") !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.homeViewWrapper {
|
|
||||||
transform: scale(2) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heatbar {
|
|
||||||
/* right: v-bind("`${props.cubeStyle.right + 2}%`") !important; */
|
|
||||||
top: 0% !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,42 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { inject, ref, watch } from "vue";
|
|
||||||
import SystemInfoModalContent from "./SystemInfoModalContent.vue";
|
|
||||||
|
|
||||||
|
|
||||||
const { selectedDevice: data } = inject("system_selectedDevice")
|
|
||||||
|
|
||||||
const position = ref({
|
|
||||||
left: "0px",
|
|
||||||
top: "0px",
|
|
||||||
display: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
data,
|
|
||||||
(newValue) => {
|
|
||||||
if (!newValue.value) {
|
|
||||||
position.value = {
|
|
||||||
display: "none"
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
position.value = {
|
|
||||||
...data.value.initPos,
|
|
||||||
display: "block"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}, {
|
|
||||||
deep: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal id="system_info_modal" :width="600" :draggable="!data?.isMobile" :backdrop="false">
|
|
||||||
<template #modalContent>
|
|
||||||
<SystemInfoModalContent />
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
@ -70,7 +70,7 @@ export default function useForgeSprite() {
|
|||||||
viewableData.spriteSize = 24;
|
viewableData.spriteSize = 24;
|
||||||
|
|
||||||
// 圖示
|
// 圖示
|
||||||
const spriteIconUrl = "/dist/img/hotSpot.svg";
|
const spriteIconUrl = "/img/hotSpot.svg";
|
||||||
|
|
||||||
STATIC_SUB_DEVICES.forEach((d) => {
|
STATIC_SUB_DEVICES.forEach((d) => {
|
||||||
if (!d.device_coordinate_3d) return;
|
if (!d.device_coordinate_3d) return;
|
||||||
|
@ -4,12 +4,14 @@
|
|||||||
>
|
>
|
||||||
<!-- 高度比重:2 -->
|
<!-- 高度比重:2 -->
|
||||||
<div class="flex-[2] grid grid-cols-4 gap-2">
|
<div class="flex-[2] grid grid-cols-4 gap-2">
|
||||||
|
<!-- 住民人數 Card -->
|
||||||
<div
|
<div
|
||||||
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl 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"
|
||||||
width="16"
|
width="16"
|
||||||
@ -28,12 +30,15 @@
|
|||||||
<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-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
||||||
|
@click="openModal('inpatients')"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center items-center gap-4">
|
<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"
|
||||||
width="16"
|
width="16"
|
||||||
@ -52,6 +57,147 @@
|
|||||||
<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">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<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-lg"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
關閉
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
||||||
>
|
>
|
||||||
@ -129,7 +275,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, computed, 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";
|
||||||
|
|
||||||
@ -292,4 +438,194 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
disposeCharts();
|
disposeCharts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ===== Modal 狀態 ===== */
|
||||||
|
const showModal = ref(false);
|
||||||
|
const modalType = ref(null);
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
if (modalType.value === "residents") return "住民資訊";
|
||||||
|
if (modalType.value === "inpatients") return "住院資訊";
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
function openModal(type) {
|
||||||
|
modalType.value = type;
|
||||||
|
showModal.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
const dialog = document.querySelector('[role="dialog"]');
|
||||||
|
if (dialog) dialog.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function closeModal() {
|
||||||
|
showModal.value = false;
|
||||||
|
modalType.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 工具:產生不含「4」的床位號 ===== */
|
||||||
|
function generateBeds(count, start = 1001) {
|
||||||
|
const arr = [];
|
||||||
|
let n = start;
|
||||||
|
while (arr.length < count) {
|
||||||
|
if (!String(n).includes("4")) arr.push(n);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 假資料:住民 ===== */
|
||||||
|
const bedsResidents = generateBeds(12, 1001);
|
||||||
|
const residentsAll = [
|
||||||
|
{
|
||||||
|
bed: bedsResidents[0],
|
||||||
|
name: "王小明",
|
||||||
|
gender: "男",
|
||||||
|
age: 78,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "喜歡下棋",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[1],
|
||||||
|
name: "陳美麗",
|
||||||
|
gender: "女",
|
||||||
|
age: 82,
|
||||||
|
condition: "需輕度協助",
|
||||||
|
note: "糖尿病控制中",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[2],
|
||||||
|
name: "林大志",
|
||||||
|
gender: "男",
|
||||||
|
age: 74,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "行動慢",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[3],
|
||||||
|
name: "張翠華",
|
||||||
|
gender: "女",
|
||||||
|
age: 80,
|
||||||
|
condition: "復健中",
|
||||||
|
note: "膝關節置換術後",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[4],
|
||||||
|
name: "黃國榮",
|
||||||
|
gender: "男",
|
||||||
|
age: 85,
|
||||||
|
condition: "需中度協助",
|
||||||
|
note: "夜間易醒",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[5],
|
||||||
|
name: "李佩珊",
|
||||||
|
gender: "女",
|
||||||
|
age: 76,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "對花粉過敏",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[6],
|
||||||
|
name: "吳大同",
|
||||||
|
gender: "男",
|
||||||
|
age: 79,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "喜歡園藝",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[7],
|
||||||
|
name: "周怡君",
|
||||||
|
gender: "女",
|
||||||
|
age: 81,
|
||||||
|
condition: "需輕度協助",
|
||||||
|
note: "高血壓",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[8],
|
||||||
|
name: "曾文龍",
|
||||||
|
gender: "男",
|
||||||
|
age: 77,
|
||||||
|
condition: "復健中",
|
||||||
|
note: "髖關節手術後",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[9],
|
||||||
|
name: "蔡淑芬",
|
||||||
|
gender: "女",
|
||||||
|
age: 83,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "喜歡編織",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[10],
|
||||||
|
name: "許建宏",
|
||||||
|
gender: "男",
|
||||||
|
age: 75,
|
||||||
|
condition: "需中度協助",
|
||||||
|
note: "睡眠品質不佳",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[11],
|
||||||
|
name: "簡婉婷",
|
||||||
|
gender: "女",
|
||||||
|
age: 78,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "對海鮮過敏",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const residentsA = residentsAll.filter((r) => r.area === "A");
|
||||||
|
const residentsB = residentsAll.filter((r) => r.area === "B");
|
||||||
|
|
||||||
|
/* ===== 假資料:住院 ===== */
|
||||||
|
const bedsInpatient = generateBeds(4, 1020);
|
||||||
|
const inpatients = [
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[0],
|
||||||
|
name: "劉書豪",
|
||||||
|
hospitalDept: "台大醫院/心臟內科",
|
||||||
|
recordNo: "A1023001",
|
||||||
|
status: "加護中",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[1],
|
||||||
|
name: "高雅筑",
|
||||||
|
hospitalDept: "榮總/新陳代謝科",
|
||||||
|
recordNo: "B1023007",
|
||||||
|
status: "住院觀察",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[2],
|
||||||
|
name: "方志明",
|
||||||
|
hospitalDept: "長庚/骨科",
|
||||||
|
recordNo: "C1023011",
|
||||||
|
status: "術後恢復",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[3],
|
||||||
|
name: "鄭于庭",
|
||||||
|
hospitalDept: "國泰/神經內科",
|
||||||
|
recordNo: "D1023020",
|
||||||
|
status: "檢查中",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ESC 關閉 */
|
||||||
|
function onKeydown(e) {
|
||||||
|
if (e.key === "Escape") closeModal();
|
||||||
|
}
|
||||||
|
onMounted(() => window.addEventListener("keydown", onKeydown));
|
||||||
|
onUnmounted(() => window.removeEventListener("keydown", onKeydown));
|
||||||
</script>
|
</script>
|
||||||
|
@ -4,12 +4,14 @@
|
|||||||
>
|
>
|
||||||
<!-- 高度比重:2 -->
|
<!-- 高度比重:2 -->
|
||||||
<div class="flex-[2] grid grid-cols-4 gap-2">
|
<div class="flex-[2] grid grid-cols-4 gap-2">
|
||||||
|
<!-- 住民人數 Card -->
|
||||||
<div
|
<div
|
||||||
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl 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"
|
||||||
width="16"
|
width="16"
|
||||||
@ -28,12 +30,15 @@
|
|||||||
<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-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
||||||
|
@click="openModal('inpatients')"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center items-center gap-4">
|
<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"
|
||||||
width="16"
|
width="16"
|
||||||
@ -52,6 +57,147 @@
|
|||||||
<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">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<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-lg"
|
||||||
|
@click="closeModal"
|
||||||
|
>
|
||||||
|
關閉
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
class="col-span-1 cursor-pointer active:opacity-80 text-white bg-brand-green rounded-xl shadow px-4 pt-3 flex flex-col justify-center items-start tracking-widest"
|
||||||
>
|
>
|
||||||
@ -129,7 +275,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
import { ref, computed, 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";
|
||||||
|
|
||||||
@ -292,4 +438,194 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
disposeCharts();
|
disposeCharts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ===== Modal 狀態 ===== */
|
||||||
|
const showModal = ref(false);
|
||||||
|
const modalType = ref(null);
|
||||||
|
|
||||||
|
const modalTitle = computed(() => {
|
||||||
|
if (modalType.value === "residents") return "住民資訊";
|
||||||
|
if (modalType.value === "inpatients") return "住院資訊";
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
|
function openModal(type) {
|
||||||
|
modalType.value = type;
|
||||||
|
showModal.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
const dialog = document.querySelector('[role="dialog"]');
|
||||||
|
if (dialog) dialog.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function closeModal() {
|
||||||
|
showModal.value = false;
|
||||||
|
modalType.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 工具:產生不含「4」的床位號 ===== */
|
||||||
|
function generateBeds(count, start = 1001) {
|
||||||
|
const arr = [];
|
||||||
|
let n = start;
|
||||||
|
while (arr.length < count) {
|
||||||
|
if (!String(n).includes("4")) arr.push(n);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 假資料:住民 ===== */
|
||||||
|
const bedsResidents = generateBeds(12, 1001);
|
||||||
|
const residentsAll = [
|
||||||
|
{
|
||||||
|
bed: bedsResidents[0],
|
||||||
|
name: "王小明",
|
||||||
|
gender: "男",
|
||||||
|
age: 78,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "喜歡下棋",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[1],
|
||||||
|
name: "陳美麗",
|
||||||
|
gender: "女",
|
||||||
|
age: 82,
|
||||||
|
condition: "需輕度協助",
|
||||||
|
note: "糖尿病控制中",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[2],
|
||||||
|
name: "林大志",
|
||||||
|
gender: "男",
|
||||||
|
age: 74,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "行動慢",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[3],
|
||||||
|
name: "張翠華",
|
||||||
|
gender: "女",
|
||||||
|
age: 80,
|
||||||
|
condition: "復健中",
|
||||||
|
note: "膝關節置換術後",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[4],
|
||||||
|
name: "黃國榮",
|
||||||
|
gender: "男",
|
||||||
|
age: 85,
|
||||||
|
condition: "需中度協助",
|
||||||
|
note: "夜間易醒",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[5],
|
||||||
|
name: "李佩珊",
|
||||||
|
gender: "女",
|
||||||
|
age: 76,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "對花粉過敏",
|
||||||
|
area: "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[6],
|
||||||
|
name: "吳大同",
|
||||||
|
gender: "男",
|
||||||
|
age: 79,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "喜歡園藝",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[7],
|
||||||
|
name: "周怡君",
|
||||||
|
gender: "女",
|
||||||
|
age: 81,
|
||||||
|
condition: "需輕度協助",
|
||||||
|
note: "高血壓",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[8],
|
||||||
|
name: "曾文龍",
|
||||||
|
gender: "男",
|
||||||
|
age: 77,
|
||||||
|
condition: "復健中",
|
||||||
|
note: "髖關節手術後",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[9],
|
||||||
|
name: "蔡淑芬",
|
||||||
|
gender: "女",
|
||||||
|
age: 83,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "喜歡編織",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[10],
|
||||||
|
name: "許建宏",
|
||||||
|
gender: "男",
|
||||||
|
age: 75,
|
||||||
|
condition: "需中度協助",
|
||||||
|
note: "睡眠品質不佳",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsResidents[11],
|
||||||
|
name: "簡婉婷",
|
||||||
|
gender: "女",
|
||||||
|
age: 78,
|
||||||
|
condition: "穩定",
|
||||||
|
note: "對海鮮過敏",
|
||||||
|
area: "B",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const residentsA = residentsAll.filter((r) => r.area === "A");
|
||||||
|
const residentsB = residentsAll.filter((r) => r.area === "B");
|
||||||
|
|
||||||
|
/* ===== 假資料:住院 ===== */
|
||||||
|
const bedsInpatient = generateBeds(4, 1020);
|
||||||
|
const inpatients = [
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[0],
|
||||||
|
name: "劉書豪",
|
||||||
|
hospitalDept: "台大醫院/心臟內科",
|
||||||
|
recordNo: "A1023001",
|
||||||
|
status: "加護中",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[1],
|
||||||
|
name: "高雅筑",
|
||||||
|
hospitalDept: "榮總/新陳代謝科",
|
||||||
|
recordNo: "B1023007",
|
||||||
|
status: "住院觀察",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[2],
|
||||||
|
name: "方志明",
|
||||||
|
hospitalDept: "長庚/骨科",
|
||||||
|
recordNo: "C1023011",
|
||||||
|
status: "術後恢復",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bed: bedsInpatient[3],
|
||||||
|
name: "鄭于庭",
|
||||||
|
hospitalDept: "國泰/神經內科",
|
||||||
|
recordNo: "D1023020",
|
||||||
|
status: "檢查中",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ESC 關閉 */
|
||||||
|
function onKeydown(e) {
|
||||||
|
if (e.key === "Escape") closeModal();
|
||||||
|
}
|
||||||
|
onMounted(() => window.addEventListener("keydown", onKeydown));
|
||||||
|
onUnmounted(() => window.removeEventListener("keydown", onKeydown));
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user