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 @@
+
+
+
+
+
+
+
+
+
+
+
+
通知
+
+
+
未完成的知會事項與代辦事項
+
+
+
+
+
+
+ 日期 |
+ 床位 |
+ 姓名 |
+ 類型 |
+ 說明 |
+ 逾期天數 |
+
+
+
+
+ {{ row.date }} |
+ {{ row.bed }} |
+ {{ row.name }} |
+ {{ row.type }} |
+ {{ row.desc }} |
+ {{ row.overdueDays }} 天 |
+
+
+
+
+ 目前沒有待辦。
+ |
+
+
+
+
+
+
+
+
+
+ 共 {{ total }} 筆,每頁 {{ pageSize }} 筆
+
+
+
+
+ {{ currentPage }} / {{ totalPages }}
+
+
+
+
+
+
+
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",