From 5cc0dcbf567f4d59146ffd6e097485376e0dd5e9 Mon Sep 17 00:00:00 2001 From: "MJM_2025_05\\polly" Date: Tue, 23 Sep 2025 11:18:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A6=96=E9=A0=81=E5=9C=B0=E5=9C=96?= =?UTF-8?q?=E5=B1=95=E9=96=8B=E8=88=87=E6=94=B6=E9=97=94=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + index.html | 2 +- package.json | 6 +- scripts/write-version.mjs | 29 +++++ src/App.vue | 175 +++++++++++++++++++++++++- src/components/common/forge/Forge.vue | 70 ++++++++++- src/components/home/MapPane.vue | 123 ++++++++++++------ src/components/home/ProgressBar.vue | 2 +- src/layouts/NavBar.vue | 33 ++++- src/main.js | 37 ++++-- src/pages/home/index.vue | 138 ++++++++++++++++---- src/pages/nursing/index.vue | 2 +- src/stores/notif.js | 27 ++++ src/utils/versionGuard.js | 52 ++++++++ tailwind.config.js | 2 +- 15 files changed, 617 insertions(+), 82 deletions(-) create mode 100644 scripts/write-version.mjs create mode 100644 src/stores/notif.js create mode 100644 src/utils/versionGuard.js diff --git a/.gitignore b/.gitignore index a547bf3..0abf3e7 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +/public/version.json diff --git a/index.html b/index.html index f3eab4e..9e8a9ad 100644 --- a/index.html +++ b/index.html @@ -14,7 +14,7 @@ 諾亞克 U-ARK 戰情中心 - +
diff --git a/package.json b/package.json index 23df67e..1adb273 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,11 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "prebuild": "node scripts/write-version.mjs", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "dev": "vite", + "version:write": "node scripts/write-version.mjs" }, "dependencies": { "@fullcalendar/core": "^6.1.19", diff --git a/scripts/write-version.mjs b/scripts/write-version.mjs new file mode 100644 index 0000000..12a91ae --- /dev/null +++ b/scripts/write-version.mjs @@ -0,0 +1,29 @@ +import { writeFileSync, mkdirSync } from 'node:fs'; +import { execSync } from 'node:child_process'; +import { resolve } from 'node:path'; + +function safe(cmd, fallback = '') { + try { + return execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'] }) + .toString() + .trim(); + } catch { + return fallback; + } +} + +const now = new Date().toISOString(); +const commit = safe('git rev-parse --short HEAD', 'local'); +const branch = safe('git rev-parse --abbrev-ref HEAD', ''); +const buildId = `${now.replace(/[-:TZ.]/g, '').slice(0, 14)}.${commit}`; + +const payload = { + version: now, // 比對用(每次 build 都不同) + commit, + branch, + buildId +}; + +mkdirSync(resolve('public'), { recursive: true }); +writeFileSync(resolve('public/version.json'), JSON.stringify(payload, null, 2), 'utf-8'); +console.log('[write-version] public/version.json ->', payload); diff --git a/src/App.vue b/src/App.vue index 117d39d..d0fdf44 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,13 +3,122 @@ + + + + + diff --git a/src/components/common/forge/Forge.vue b/src/components/common/forge/Forge.vue index e5c0ba4..946f491 100644 --- a/src/components/common/forge/Forge.vue +++ b/src/components/common/forge/Forge.vue @@ -200,6 +200,29 @@ /> + +
+ +
+
/** ===================== Imports ===================== */ -import { ref, computed, onMounted, onUnmounted, watch } from "vue"; +import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"; import { useRoute } from "vue-router"; import useForgeSprite from "@/hooks/forge/useForgeSprite"; import Tabs from "@/components/common/tabs/Tabs.vue"; @@ -318,6 +341,9 @@ import { RESIDENTS_BY_BED, } from "@/constants/mocks/facilityData"; +let INITIAL_1F_STATE = null; // 儲存 1F 初始視角(含樓層隔離/相機/切面等) +let INITIAL_1F_CAMERA = null; // 相機詳細快照 + /** ===================== Tabs:樓層/狀態/分區 ===================== */ const floorTabItems = [ { value: "1F", label: "1F" }, @@ -953,12 +979,14 @@ function clearOurTheming(model) { } catch {} lastThemedIds.clear(); } + function setRoomTheming(id, rgba) { const THREE = getThree(); const c = new THREE.Vector4(rgba.r, rgba.g, rgba.b, rgba.a); viewer.setThemingColor(id, c, viewer.model, true); lastThemedIds.add(id); } + function rebuildZoneThemingForCurrentFloor() { if (!viewer || !viewer.model) return; clearOurTheming(viewer.model); @@ -1020,6 +1048,34 @@ function rebuildZoneThemingForCurrentFloor() { viewer.impl.invalidate(true, true, true); } +/** ===================== 返回模型初始視角 ===================== */ +async function goHome() { + if (!viewer) return; + + // 確保樓層先回到 1F(會重建 sprites 與隔離) + await applyFloor("1F"); + + // 套回初始快照(相機/隔離/等),第三參數 immediate=true 立即更新 + if (INITIAL_1F_STATE) { + try { + viewer.restoreState(INITIAL_1F_STATE, undefined, true); + } catch (e) { + // 萬一還原失敗就退回 fitToView 方案 + const id = FLOOR_DBIDS["1F"]; + if (id != null) { + const ids = collectLeafsUnder(viewer.model, id); + viewer.fitToView?.(ids, viewer.model); + } else { + viewer.fitToView?.(); + } + } + } + + // 依舊重算 popover 與區塊上色(避免狀態還原與你自訂邏輯不同步) + rebuildLabelsAfterNextRender(); + rebuildZoneThemingForCurrentFloor(); +} + /** ===================== 快照輸出(供 operation 使用) ===================== */ function parseBedCodeFromLabel(label) { const m = String(label || "").match(/床號\s*([0-9]{3}(?:-[1-3])?)/); @@ -1628,6 +1684,18 @@ onMounted(async () => { }); attachCameraEventsForLabels(); await applyFloor("1F"); + + // 等一禎,確保 fitToView/隔離/labels 都完成再抓狀態(更穩定) + await new Promise((r) => { + const once = () => { + viewer.removeEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, once); + r(); + }; + viewer.addEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, once); + }); + + INITIAL_1F_STATE = viewer.getState(); // 記錄整體 viewer 狀態做為 Home + viewerReady.value = true; }); diff --git a/src/components/home/MapPane.vue b/src/components/home/MapPane.vue index b365ebb..140dad8 100644 --- a/src/components/home/MapPane.vue +++ b/src/components/home/MapPane.vue @@ -30,11 +30,28 @@ diff --git a/src/pages/nursing/index.vue b/src/pages/nursing/index.vue index bc834a0..15df453 100644 --- a/src/pages/nursing/index.vue +++ b/src/pages/nursing/index.vue @@ -74,7 +74,7 @@ {{ row.name }} {{ row.type }} {{ row.desc }} - + {{ row.overdueDays ?? 0 }} 天 diff --git a/src/stores/notif.js b/src/stores/notif.js new file mode 100644 index 0000000..ad14098 --- /dev/null +++ b/src/stores/notif.js @@ -0,0 +1,27 @@ +import { defineStore } from "pinia"; + +export const useNotifStore = defineStore("notif", { + state: () => ({ + isOpen: false, + _lastActiveEl: null, // 可選:關閉後把焦點還回觸發鈴鐺 + }), + actions: { + open() { + this._lastActiveEl = document.activeElement; + this.isOpen = true; + document.documentElement.style.overflow = "hidden"; // 鎖背景捲動 + }, + close() { + this.isOpen = false; + document.documentElement.style.overflow = ""; + // 還原焦點(a11y) + if (this._lastActiveEl && this._lastActiveEl.focus) { + this._lastActiveEl.focus(); + } + this._lastActiveEl = null; + }, + toggle() { + this.isOpen ? this.close() : this.open(); + }, + }, +}); diff --git a/src/utils/versionGuard.js b/src/utils/versionGuard.js new file mode 100644 index 0000000..eee20d7 --- /dev/null +++ b/src/utils/versionGuard.js @@ -0,0 +1,52 @@ +// src/utils/versionGuard.js +const VERSION_URL = '/version.json'; +const LS_KEY = '__app_version__'; +const SESSION_REFRESH_FLAG = '__just_refreshed__'; + +async function fetchRemoteVersion() { + const res = await fetch(VERSION_URL, { cache: 'no-store' }); // ★ 關鍵:不快取 + if (!res.ok) throw new Error('version.json fetch failed'); + return res.json(); +} + +async function unregisterAllSW() { + if (!('serviceWorker' in navigator)) return; + const regs = await navigator.serviceWorker.getRegistrations(); + await Promise.all(regs.map(r => r.unregister())); +} + +async function clearCacheStorage() { + if (!('caches' in window)) return; + const keys = await caches.keys(); + await Promise.all(keys.map(k => caches.delete(k))); +} + +export async function ensureFreshAssets() { + try { + const prev = localStorage.getItem(LS_KEY); + const remote = await fetchRemoteVersion(); + const ver = remote.version || remote.buildId || remote.commit; + if (!ver) return; + + if (prev && prev !== ver) { + await unregisterAllSW(); + await clearCacheStorage(); + sessionStorage.setItem(SESSION_REFRESH_FLAG, '1'); + // 用 replace 避免回上一頁又載到舊 index.html + location.replace(location.href.split('#')[0]); + return; + } + + if (!prev) localStorage.setItem(LS_KEY, ver); + } catch (e) { + console.warn('[versionGuard] check failed:', e); + } +} + +export async function syncLocalVersion() { + try { + const remote = await fetchRemoteVersion(); + const ver = remote.version || remote.buildId || remote.commit; + if (ver) localStorage.setItem(LS_KEY, ver); + } catch {} +} diff --git a/tailwind.config.js b/tailwind.config.js index 33277e7..0603537 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -46,7 +46,7 @@ module.exports = { lighter: "#F2F9F6", dark: "#0CA99C", }, - red: "#FF8678", + red: { DEFAULT: "#FF8678", dark: "#F26252" }, purple: { DEFAULT: "#A5BEFF", light: "#D5E1FF",