From c4d78bd5b6cc77eaa764e726c7392ba93b5aea62 Mon Sep 17 00:00:00 2001 From: "MJM_2025_05\\polly" Date: Thu, 21 Aug 2025 15:31:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A3=BD=E4=BD=9C=20demo=20=E7=94=A8?= =?UTF-8?q?=20modals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/img/hotSpot.svg | 21 ++ src/components/forge/Forge.vue | 367 ++++++++++++++++-------- src/components/forge/ForgeForSystem.vue | 282 ------------------ src/components/forge/ForgeInfoModal.vue | 42 --- src/hooks/forge/useForgeSprite.js | 2 +- src/pages/home/index.vue | 342 +++++++++++++++++++++- src/pages/operation/index.vue | 342 +++++++++++++++++++++- 7 files changed, 955 insertions(+), 443 deletions(-) create mode 100644 public/img/hotSpot.svg delete mode 100644 src/components/forge/ForgeForSystem.vue delete mode 100644 src/components/forge/ForgeInfoModal.vue diff --git a/public/img/hotSpot.svg b/public/img/hotSpot.svg new file mode 100644 index 0000000..2b2249a --- /dev/null +++ b/public/img/hotSpot.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/forge/Forge.vue b/src/components/forge/Forge.vue index 2cc7776..fea8b32 100644 --- a/src/components/forge/Forge.vue +++ b/src/components/forge/Forge.vue @@ -38,8 +38,6 @@ const FLOOR_DBIDS = { "9F": 14973, "8F": 14262 }; // ====== 顏色 / Sprite數量(可調) ====== const BRAND_GREEN = "#34D5C8"; const BRAND_RED = "#FF8678"; -const SPRITES_PER_FLOOR = 6; -const RED_PER_FLOOR = 2; // (可調)本地 SVF 路徑(相對於 public) const MODEL_SVF_PATH = `/upload/forge/0.svf`; @@ -50,6 +48,37 @@ let viewer = null; const viewerReady = ref(false); 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 還原 function resolveInitialFloor() { const q = (route.query?.floor || "").toString().toUpperCase(); @@ -132,27 +161,6 @@ function getNodeWorldBounds(model, dbId) { 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 位置(固定佈局用) function positionsFromRatios(box, ratios, zLiftRatio = 0.05) { const THREE = getThree(); @@ -242,6 +250,15 @@ async function rebuildSpritesForFloor(floorKey) { residentsSex: pick(SEX_POOL, spriteDbId + 1), residentsAge: ageFromSeed(spriteDbId + 2), 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() { + camEvtCleanup?.(); // 先清掉舊的 const onUpdate = () => requestAnimationFrame(updateLabelPositions); viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, onUpdate); viewer.addEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, onUpdate); @@ -342,8 +360,7 @@ async function onClickFloor(next) { } onMounted(async () => { - // 強制首頁預設 9F(避免舊的 localStorage/路由殘值影響) - activeFloor.value = "9F"; + activeFloor.value = resolveInitialFloor(); await initViewer(forgeDom.value); 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 onClickZone = (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 = ["男", "女"]; function seededRand(seed) { const s = Math.sin(seed * 9301 + 49297) * 233280; @@ -475,37 +519,51 @@ function startDateFromSeed(seed) { return `${d.getFullYear()}/${mm}/${dd}`; } -// 選單選項(可自行增減) +// 選單選項 const infoOptions = [ { label: "無顯示", value: "none" }, { label: "有住民", value: "occupied" }, { label: "空床", value: "vacant" }, { label: "住院中", value: "hospitalized" }, + { label: "請假中", value: "leave" }, // ← 新增 ]; const filteredLabels = computed(() => { - // 單獨顯示模式:只回傳被點擊的那一顆 + // 單獨顯示模式(保留你原本的行為) if (soloSpriteId.value != null) { + if (selectedInfo.value === "none" || selectedInfo.value === "leave") + return []; return labels.value.filter((L) => L.id === soloSpriteId.value); } - // ---- 以下為全域篩選(依 infoOptions + 分區)---- - const info = selectedInfo.value; // none / occupied / vacant / hospitalized - const zone = activeZone.value; // 全部 / VIP / 換管 / 失能 / 失智(若你有真的 zone 欄位可改用) + // none / leave = 全隱(依你前面需求) + if (selectedInfo.value === "none" || selectedInfo.value === "leave") { + return []; + } + + const info = selectedInfo.value; // 'occupied' | 'vacant' | 'hospitalized' + const selectedSet = selectedZones.value; // Set 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 isHosp = isRed && !!L.data.special; // 住院中:紅 + special - const isGreen = !isRed && !L.data.special; // 有住民:綠(依你資料邏輯) + const isGreen = !isRed && !L.data.special; // 有住民:綠 let stateOk = true; - if (info === "none") stateOk = false; // 無顯示 → 全隱(配合需求 2) - if (info === "occupied") stateOk = isGreen; // 有住民 → 綠 - if (info === "vacant") stateOk = isRed; // 空床 → 紅(無論 special 與否) - if (info === "hospitalized") stateOk = isHosp; // 住院中 → 紅+special + if (info === "occupied") stateOk = isGreen; + if (info === "vacant") stateOk = isRed; // 空床 = 只要是紅色,都包含(含住院) + if (info === "hospitalized") stateOk = isHosp; return inZone && stateOk; }); @@ -588,8 +646,21 @@ onUnmounted(() => { @click="bringToFrontById(L.id)" >
+ + +
- -
+ +
{
- -
-
-
- {{ infoDisplay }} - - - - - -
- - -
-
    -
  • - {{ opt.label }} -
  • -
-
-
-
- - +

分區|

+
- +
-

圖例|

-
-
+

床位資訊|

+ +
+ + + + +
-
+ 有住民 + + + +
-
+ 空床 + + + +
+ 住院中 + + + +
+ + + diff --git a/src/components/forge/ForgeInfoModal.vue b/src/components/forge/ForgeInfoModal.vue deleted file mode 100644 index 0917efe..0000000 --- a/src/components/forge/ForgeInfoModal.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - - diff --git a/src/hooks/forge/useForgeSprite.js b/src/hooks/forge/useForgeSprite.js index bd66e3f..372e111 100644 --- a/src/hooks/forge/useForgeSprite.js +++ b/src/hooks/forge/useForgeSprite.js @@ -70,7 +70,7 @@ export default function useForgeSprite() { viewableData.spriteSize = 24; // 圖示 - const spriteIconUrl = "/dist/img/hotSpot.svg"; + const spriteIconUrl = "/img/hotSpot.svg"; STATIC_SUB_DEVICES.forEach((d) => { if (!d.device_coordinate_3d) return; diff --git a/src/pages/home/index.vue b/src/pages/home/index.vue index dbdbec2..a903be5 100644 --- a/src/pages/home/index.vue +++ b/src/pages/home/index.vue @@ -4,12 +4,14 @@ >
+

住民人數

- +

+ +

住院人數

- +

+ + +
+ +
+ + + +
+
@@ -129,7 +275,7 @@ diff --git a/src/pages/operation/index.vue b/src/pages/operation/index.vue index dbdbec2..a903be5 100644 --- a/src/pages/operation/index.vue +++ b/src/pages/operation/index.vue @@ -4,12 +4,14 @@ >
+

住民人數

- +

+ +

住院人數

- +

+ + +
+ +
+ + + +
+
@@ -129,7 +275,7 @@