fix: 修正文字顏色錯誤套用瀏覽器 darkMode 問題
This commit is contained in:
parent
c4d78bd5b6
commit
cb74fdb0f2
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
@ -1,4 +1,3 @@
|
||||
<!-- src/App.vue -->
|
||||
<template>
|
||||
<section id="app" class="flex flex-col min-h-screen">
|
||||
<NavBar />
|
||||
|
@ -1,2 +0,0 @@
|
||||
export const GET_FORGETOKEN_API = `/api/forge/oauth/token`;
|
||||
export const GET_FORGEURN_API = `/api/Device/GetBuild`
|
@ -1,33 +0,0 @@
|
||||
// import instance from "@/util/request";
|
||||
import { GET_FORGETOKEN_API, GET_FORGEURN_API } from "./apis";
|
||||
// import apihandler from "@/util/apihandler";
|
||||
|
||||
// 取得 URN 清單(維持你的行為)
|
||||
export async function getUrn() {
|
||||
const res = await instance.post(GET_FORGEURN_API);
|
||||
return apihandler(res.code, res.data, { msg: res.msg, code: res.code });
|
||||
}
|
||||
|
||||
// 取得 Token(Promise 版)
|
||||
export async function fetchForgeToken() {
|
||||
const resp = await instance.get(GET_FORGETOKEN_API);
|
||||
// 兼容兩種常見回傳格式:
|
||||
// A) { dictionary: { access_token, expires_in } }
|
||||
// B) { access_token, expires_in }
|
||||
const dict = resp?.dictionary ?? resp;
|
||||
if (!dict?.access_token || !dict?.expires_in) {
|
||||
throw new Error("Invalid token response shape");
|
||||
}
|
||||
return { access_token: dict.access_token, expires_in: dict.expires_in };
|
||||
}
|
||||
|
||||
// 兼容舊的 callback 界面(給 Viewer 的 getAccessToken 用)
|
||||
export async function getAccessToken(callback) {
|
||||
try {
|
||||
const { access_token, expires_in } = await fetchForgeToken();
|
||||
callback(access_token, expires_in);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Could not obtain access token. See the console for more details.");
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import { fetchForgeToken } from "./index";
|
||||
|
||||
let cached = null;
|
||||
|
||||
export async function getCachedToken() {
|
||||
const now = Date.now();
|
||||
if (cached && now < cached.expireAt) {
|
||||
return cached;
|
||||
}
|
||||
const { access_token, expires_in } = await fetchForgeToken();
|
||||
// 提前 30 秒更新,避免臨界點過期
|
||||
cached = {
|
||||
access_token,
|
||||
expires_in,
|
||||
expireAt: now + (expires_in - 30) * 1000,
|
||||
};
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 提供給 Viewer 的介面
|
||||
export async function viewerGetAccessToken(onTokenReady) {
|
||||
const { access_token, expires_in } = await getCachedToken();
|
||||
onTokenReady(access_token, expires_in);
|
||||
}
|
@ -1,10 +1,17 @@
|
||||
<!-- src/components/forge/Forge.vue -->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import useForgeSprite from "@/hooks/forge/useForgeSprite";
|
||||
|
||||
// ====== 基本變數 / 注入 ======
|
||||
const route = useRoute();
|
||||
const forgeDom = ref(null);
|
||||
let viewer = null;
|
||||
const viewerReady = ref(false);
|
||||
const activeFloor = ref("9F"); // 預設 9F
|
||||
|
||||
let unbindSpriteClick = null;
|
||||
let THREE = null;
|
||||
|
||||
const {
|
||||
updateDataVisualization: dvInit,
|
||||
@ -15,9 +22,13 @@ const {
|
||||
__staticDevices, // 用來塞模擬資料
|
||||
} = useForgeSprite();
|
||||
|
||||
let THREE = null;
|
||||
// ====== 常數(樓層 / 顏色 / 模型路徑) ======
|
||||
const FLOOR_DBIDS = { "9F": 14973, "8F": 14262 };
|
||||
const BRAND_GREEN = "#34D5C8";
|
||||
const BRAND_RED = "#FF8678";
|
||||
const MODEL_SVF_PATH = `/upload/forge/0.svf`;
|
||||
|
||||
// 取得 Viewer 內建 THREE(避免版本衝突)
|
||||
// ====== THREE 入口(避免版本衝突) ======
|
||||
function getThree() {
|
||||
if (THREE) return THREE;
|
||||
THREE =
|
||||
@ -32,23 +43,7 @@ function getThree() {
|
||||
return THREE;
|
||||
}
|
||||
|
||||
// ====== 寫死樓層 dbId(你提供的 Log) ======
|
||||
const FLOOR_DBIDS = { "9F": 14973, "8F": 14262 };
|
||||
|
||||
// ====== 顏色 / Sprite數量(可調) ======
|
||||
const BRAND_GREEN = "#34D5C8";
|
||||
const BRAND_RED = "#FF8678";
|
||||
|
||||
// (可調)本地 SVF 路徑(相對於 public)
|
||||
const MODEL_SVF_PATH = `/upload/forge/0.svf`;
|
||||
|
||||
const route = useRoute();
|
||||
const forgeDom = ref(null);
|
||||
let viewer = null;
|
||||
const viewerReady = ref(false);
|
||||
const activeFloor = ref("9F"); // 預設 9F
|
||||
|
||||
// ---- Resident Modal 狀態 ----
|
||||
// ====== Resident Modal 狀態 ======
|
||||
const modalOpen = ref(false);
|
||||
const modalData = ref(null);
|
||||
|
||||
@ -69,7 +64,7 @@ function buildResidentModalData(d) {
|
||||
}
|
||||
|
||||
function openResidentModal(L, e) {
|
||||
e?.stopPropagation?.(); // 避免事件冒泡
|
||||
e?.stopPropagation?.();
|
||||
bringToFrontById(L.id);
|
||||
modalData.value = buildResidentModalData(L.data);
|
||||
modalOpen.value = true;
|
||||
@ -79,7 +74,7 @@ function closeResidentModal() {
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
// 嘗試從 ?floor=8F|9F 或 localStorage 還原
|
||||
// ====== 初始化樓層還原 ======
|
||||
function resolveInitialFloor() {
|
||||
const q = (route.query?.floor || "").toString().toUpperCase();
|
||||
if (q === "8F" || q === "9F") return q;
|
||||
@ -88,7 +83,7 @@ function resolveInitialFloor() {
|
||||
return "9F";
|
||||
}
|
||||
|
||||
// 初始化 Forge Viewer
|
||||
// ====== Forge Viewer 初始化 / 載入 ======
|
||||
function initViewer(container) {
|
||||
return new Promise((resolve) => {
|
||||
Autodesk.Viewing.Initializer({ env: "Local", language: "en" }, () => {
|
||||
@ -108,14 +103,12 @@ function initViewer(container) {
|
||||
});
|
||||
}
|
||||
|
||||
// 載入模型
|
||||
function loadModel(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
viewer.loadModel(filePath, {}, (model) => resolve(model), reject);
|
||||
});
|
||||
}
|
||||
|
||||
// 隱藏 ViewCube + Home(右上角)
|
||||
function hideViewCubeAndHome() {
|
||||
const tryHide = () => {
|
||||
const ext = viewer?.getExtension?.("Autodesk.ViewCubeUi");
|
||||
@ -127,7 +120,6 @@ function hideViewCubeAndHome() {
|
||||
});
|
||||
}
|
||||
|
||||
// 等待物件樹就緒
|
||||
function waitObjectTree(model) {
|
||||
return new Promise((resolve) => {
|
||||
if (model.getData().instanceTree) return resolve();
|
||||
@ -142,7 +134,7 @@ function waitObjectTree(model) {
|
||||
});
|
||||
}
|
||||
|
||||
// 取得某節點(樓層)的世界包圍盒
|
||||
// ====== 幾何工具 ======
|
||||
function getNodeWorldBounds(model, dbId) {
|
||||
const THREE = getThree();
|
||||
const tree = model.getInstanceTree?.();
|
||||
@ -161,7 +153,6 @@ function getNodeWorldBounds(model, dbId) {
|
||||
return box;
|
||||
}
|
||||
|
||||
// 把 [0~1] 比例座標轉成實際 3D 位置(固定佈局用)
|
||||
function positionsFromRatios(box, ratios, zLiftRatio = 0.05) {
|
||||
const THREE = getThree();
|
||||
const { min, max } = box;
|
||||
@ -175,7 +166,6 @@ function positionsFromRatios(box, ratios, zLiftRatio = 0.05) {
|
||||
}
|
||||
|
||||
// 每層的「不工整」固定佈局(相對比例)
|
||||
// 9F: 6 顆(刻意拉開間距與方向),8F: 2 顆
|
||||
const LAYOUT_RATIOS = {
|
||||
"9F": [
|
||||
[0.18, 0.26],
|
||||
@ -191,11 +181,75 @@ const LAYOUT_RATIOS = {
|
||||
],
|
||||
};
|
||||
|
||||
// 顯示指定樓層(只 isolate + fit)
|
||||
// ====== 床號序列(避開數字 4) ======
|
||||
function hasDigit4(n) {
|
||||
return n.toString().includes("4");
|
||||
}
|
||||
function generateBedNumbers(count, start = 1001) {
|
||||
const out = [];
|
||||
let n = start;
|
||||
while (out.length < count) {
|
||||
if (!hasDigit4(n)) out.push(n);
|
||||
n++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const BED_SERIES_ALL = generateBedNumbers(8, 1001);
|
||||
const BED_SERIES_BY_FLOOR = {
|
||||
"9F": BED_SERIES_ALL.slice(0, 6),
|
||||
"8F": BED_SERIES_ALL.slice(6, 8),
|
||||
};
|
||||
|
||||
// ====== 假資料 / 權重工具 ======
|
||||
function seededRand(seed) {
|
||||
const s = Math.sin(seed * 9301 + 49297) * 233280;
|
||||
return s - Math.floor(s);
|
||||
}
|
||||
function pick(arr, seed) {
|
||||
return arr[Math.floor(seededRand(seed) * arr.length)];
|
||||
}
|
||||
function ageFromSeed(seed) {
|
||||
return 65 + Math.floor(seededRand(seed + 7) * 31);
|
||||
} // 65~95
|
||||
function startDateFromSeed(seed) {
|
||||
const now = new Date();
|
||||
const daysAgo = 30 + Math.floor(seededRand(seed + 13) * 365); // 1~12 個月內
|
||||
const d = new Date(now.getTime() - daysAgo * 86400000);
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
return `${d.getFullYear()}/${mm}/${dd}`;
|
||||
}
|
||||
|
||||
const NAME_POOL = [
|
||||
"王雅雯",
|
||||
"陳志明",
|
||||
"林冠廷",
|
||||
"張淑芬",
|
||||
"黃子涵",
|
||||
"李建宏",
|
||||
"吳佩珊",
|
||||
"周育賢",
|
||||
"趙雅筑",
|
||||
"許庭瑋",
|
||||
"郭怡君",
|
||||
"洪嘉文",
|
||||
];
|
||||
const HEALTH_POOL = ["一般", "良好", "需觀察", "虛弱"];
|
||||
const MEDICATION_POOL = ["規律服藥", "偶爾忘記", "暫停服藥", "需家屬協助"];
|
||||
const SPECIAL_POOL = ["-"];
|
||||
const NOTE_POOL = ["-", "對花生過敏", "家屬每週三探視", "夜間需加護理巡房"];
|
||||
const SEX_POOL = ["男", "女"];
|
||||
|
||||
function pickAreaLetter(seed) {
|
||||
const r = seededRand(seed);
|
||||
if (r < 0.4) return "A";
|
||||
return "B";
|
||||
}
|
||||
|
||||
// ====== 樓層顯示 / Sprites 建立 ======
|
||||
async function showLevel(levelKey) {
|
||||
const levelDbId = FLOOR_DBIDS[levelKey];
|
||||
if (!viewer || !viewer.model || !levelDbId) return;
|
||||
// 取所有葉節點 → isolate
|
||||
const ids = (() => {
|
||||
const tree = viewer.model.getInstanceTree();
|
||||
const leafs = [];
|
||||
@ -212,16 +266,14 @@ async function showLevel(levelKey) {
|
||||
viewer.setGhosting(false);
|
||||
viewer.impl.invalidate(true);
|
||||
}
|
||||
// 依指定樓層,重新產生該樓層 6 顆 sprites 並繪製
|
||||
|
||||
async function rebuildSpritesForFloor(floorKey) {
|
||||
const box = getNodeWorldBounds(viewer.model, FLOOR_DBIDS[floorKey]);
|
||||
const is9F = floorKey === "9F";
|
||||
|
||||
// 固定不工整的位置(由比例轉 3D)
|
||||
const ratios = LAYOUT_RATIOS[floorKey] || [];
|
||||
const poses = positionsFromRatios(box, ratios);
|
||||
|
||||
// 依樓層取對應的「避 4」病床號序列
|
||||
const bedNos =
|
||||
BED_SERIES_BY_FLOOR[floorKey] || generateBedNumbers(poses.length, 1001);
|
||||
|
||||
@ -251,7 +303,7 @@ async function rebuildSpritesForFloor(floorKey) {
|
||||
residentsAge: ageFromSeed(spriteDbId + 2),
|
||||
startTime: startDateFromSeed(spriteDbId + 3),
|
||||
|
||||
avatarUrl: Math.random() > 0.4 ? pick(AVATAR_POOL, spriteDbId + 4) : null,
|
||||
avatarUrl: Math.random() > 0.4 ? pick(spriteDbId + 4) : null,
|
||||
healthStatus: pick(HEALTH_POOL, spriteDbId + 5),
|
||||
medicationStatus: pick(MEDICATION_POOL, spriteDbId + 6),
|
||||
specialEvent:
|
||||
@ -262,7 +314,7 @@ async function rebuildSpritesForFloor(floorKey) {
|
||||
});
|
||||
});
|
||||
|
||||
await createSprites(); // 清舊加新(不改 scale,維持 1.0)
|
||||
await createSprites(); // 清舊加新
|
||||
}
|
||||
|
||||
// ====== Popover(常駐在每顆 sprite 上方) ======
|
||||
@ -280,7 +332,6 @@ function worldToScreen(pos3) {
|
||||
}
|
||||
|
||||
function rebuildLabelsForCurrentFloor() {
|
||||
// 依 __staticDevices 建立對應的 label
|
||||
labels.value = __staticDevices.map((d) => {
|
||||
const { x, y } = worldToScreen(
|
||||
new (getThree().Vector3)(
|
||||
@ -330,7 +381,6 @@ function attachCameraEventsForLabels() {
|
||||
|
||||
function rebuildLabelsAfterNextRender() {
|
||||
const once = () => {
|
||||
// 渲染完成後,用最新相機矩陣重建並立刻再校正一次
|
||||
rebuildLabelsForCurrentFloor();
|
||||
updateLabelPositions();
|
||||
viewer.removeEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, once);
|
||||
@ -338,7 +388,97 @@ function rebuildLabelsAfterNextRender() {
|
||||
viewer.addEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, once);
|
||||
}
|
||||
|
||||
// 整合:套樓層(不再處理 sprites,sprites 交給 hook)
|
||||
// ====== 分區切換(多選|預設不選=顯示全部) ======
|
||||
const zones = ["VIP", "換管", "失能", "失智"];
|
||||
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 soloSpriteId = ref(null); // 單獨顯示:被點擊 sprite 的 id(null 表全域/多顯示模式)
|
||||
const infoOptions = [
|
||||
{ label: "無顯示", value: "none" },
|
||||
{ label: "有住民", value: "occupied" },
|
||||
{ label: "空床", value: "vacant" },
|
||||
{ label: "住院中", value: "hospitalized" },
|
||||
{ label: "請假中", value: "leave" }, // ← 新增
|
||||
];
|
||||
const infoOpen = ref(false);
|
||||
const selectedInfo = ref(infoOptions[0].value); // 'none'
|
||||
|
||||
const filteredLabels = computed(() => {
|
||||
// 單獨顯示模式
|
||||
if (soloSpriteId.value != null) {
|
||||
if (selectedInfo.value === "none" || selectedInfo.value === "leave")
|
||||
return [];
|
||||
return labels.value.filter((L) => L.id === soloSpriteId.value);
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
// ===== 分區多選判斷 =====
|
||||
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; // 有住民:綠
|
||||
|
||||
let stateOk = true;
|
||||
if (info === "occupied") stateOk = isGreen;
|
||||
if (info === "vacant") stateOk = isRed; // 空床 = 只要是紅色,都包含(含住院)
|
||||
if (info === "hospitalized") stateOk = isHosp;
|
||||
|
||||
return inZone && stateOk;
|
||||
});
|
||||
});
|
||||
|
||||
const isVacantWithoutSpecial = (L) =>
|
||||
L?.data?.state === "offnormal" && !L?.data?.special;
|
||||
|
||||
const emit = defineEmits(["change"]);
|
||||
const selectInfo = (opt) => {
|
||||
// 切換全域顯示 → 關閉單獨顯示
|
||||
soloSpriteId.value = null;
|
||||
|
||||
selectedInfo.value = opt.value;
|
||||
infoOpen.value = false;
|
||||
emit("change", opt.value);
|
||||
};
|
||||
|
||||
const infoTriggerRef = ref(null);
|
||||
const infoPanelRef = ref(null);
|
||||
|
||||
const onClickOutsideInfo = (e) => {
|
||||
const t = e.target;
|
||||
if (!infoTriggerRef.value || !infoPanelRef.value) return;
|
||||
const insideTrigger = infoTriggerRef.value.contains(t);
|
||||
const insidePanel = infoPanelRef.value.contains(t);
|
||||
if (!insideTrigger && !insidePanel) infoOpen.value = false;
|
||||
};
|
||||
|
||||
const onKeydownInfo = (e) => {
|
||||
if (e.key === "Escape") infoOpen.value = false;
|
||||
};
|
||||
|
||||
// ====== 套用樓層 / 切換 ======
|
||||
async function applyFloor(next) {
|
||||
// 進新樓層前清空單獨顯示
|
||||
soloSpriteId.value = null;
|
||||
@ -346,19 +486,18 @@ async function applyFloor(next) {
|
||||
activeFloor.value = next;
|
||||
localStorage.setItem("uark-floor", next);
|
||||
await showLevel(next);
|
||||
// 只畫當前樓層的 sprites
|
||||
await rebuildSpritesForFloor(next);
|
||||
await rebuildSpritesForFloor(next); // 只畫當前樓層的 sprites
|
||||
viewer.impl.invalidate(true); // 強制要求一個新幀
|
||||
rebuildLabelsAfterNextRender(); // 等下一次渲染完成再對齊 labels
|
||||
rebuildLabelsAfterNextRender(); // 等下一次渲染完成再對齊 labels
|
||||
}
|
||||
|
||||
// 點擊切換樓層
|
||||
async function onClickFloor(next) {
|
||||
if (next === activeFloor.value) return;
|
||||
if (!viewerReady.value) return;
|
||||
await applyFloor(next);
|
||||
}
|
||||
|
||||
// ====== 生命週期 ======
|
||||
onMounted(async () => {
|
||||
activeFloor.value = resolveInitialFloor();
|
||||
|
||||
@ -367,10 +506,10 @@ onMounted(async () => {
|
||||
await waitObjectTree(model);
|
||||
hideViewCubeAndHome();
|
||||
|
||||
// ① 讓 DataVisualization 綁定這個 viewer
|
||||
// 讓 DataVisualization 綁定這個 viewer
|
||||
await dvInit(viewer);
|
||||
|
||||
// ④ 綁定 sprite 點擊(聚焦 + scale=2)
|
||||
// 綁定 sprite 點擊
|
||||
unbindSpriteClick = forgeClickListener(async ({ data }) => {
|
||||
// 進入「單獨顯示」模式,只顯示被點到那顆 popover
|
||||
soloSpriteId.value =
|
||||
@ -389,7 +528,11 @@ onMounted(async () => {
|
||||
viewerReady.value = true; // 放在 applyFloor 之後較穩
|
||||
});
|
||||
|
||||
// 監聽 ?floor=8F|9F
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", onClickOutsideInfo);
|
||||
document.addEventListener("keydown", onKeydownInfo);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query?.floor,
|
||||
async (v) => {
|
||||
@ -421,209 +564,7 @@ onUnmounted(() => {
|
||||
viewer.finish?.();
|
||||
viewer = null;
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
console.log("選到分區:", zone);
|
||||
};
|
||||
|
||||
// 產生不含數字 4 的連號
|
||||
function hasDigit4(n) {
|
||||
return n.toString().includes("4");
|
||||
}
|
||||
function generateBedNumbers(count, start = 1001) {
|
||||
const out = [];
|
||||
let n = start;
|
||||
while (out.length < count) {
|
||||
if (!hasDigit4(n)) out.push(n);
|
||||
n++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// 整體序列:9F 6 個 + 8F 2 個(固定不變)
|
||||
const BED_SERIES_ALL = generateBedNumbers(8, 1001);
|
||||
const BED_SERIES_BY_FLOOR = {
|
||||
"9F": BED_SERIES_ALL.slice(0, 6),
|
||||
"8F": BED_SERIES_ALL.slice(6, 8),
|
||||
};
|
||||
|
||||
// 權重選區
|
||||
function pickAreaLetter(seed) {
|
||||
const r = seededRand(seed);
|
||||
if (r < 0.4) return "A";
|
||||
return "B";
|
||||
}
|
||||
|
||||
// 假資料池(長照住民)
|
||||
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;
|
||||
return s - Math.floor(s);
|
||||
}
|
||||
function pick(arr, seed) {
|
||||
return arr[Math.floor(seededRand(seed) * arr.length)];
|
||||
}
|
||||
function ageFromSeed(seed) {
|
||||
return 65 + Math.floor(seededRand(seed + 7) * 31);
|
||||
} // 65~95
|
||||
function startDateFromSeed(seed) {
|
||||
const now = new Date();
|
||||
const daysAgo = 30 + Math.floor(seededRand(seed + 13) * 365); // 1~12 個月內
|
||||
const d = new Date(now.getTime() - daysAgo * 86400000);
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
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);
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
// ===== 分區多選判斷 =====
|
||||
// 有選任何分區 → 必須命中其中之一
|
||||
// 沒選任何分區 → 不過濾(視為全部)
|
||||
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; // 有住民:綠
|
||||
|
||||
let stateOk = true;
|
||||
if (info === "occupied") stateOk = isGreen;
|
||||
if (info === "vacant") stateOk = isRed; // 空床 = 只要是紅色,都包含(含住院)
|
||||
if (info === "hospitalized") stateOk = isHosp;
|
||||
|
||||
return inZone && stateOk;
|
||||
});
|
||||
});
|
||||
|
||||
// 單獨顯示:被點擊 sprite 的 id(null 表全域/多顯示模式)
|
||||
const soloSpriteId = ref(null);
|
||||
|
||||
// 判斷是否為空床(紅色 & 無 special)
|
||||
const isVacantWithoutSpecial = (L) =>
|
||||
L?.data?.state === "offnormal" && !L?.data?.special;
|
||||
|
||||
// 對外通知(可接父層)
|
||||
const emit = defineEmits(["change"]);
|
||||
//
|
||||
// 狀態:是否開啟 / 當前選中
|
||||
const infoOpen = ref(false);
|
||||
const selectedInfo = ref(infoOptions[0].value); // 'none'
|
||||
|
||||
const infoDisplay = computed(() => {
|
||||
return (
|
||||
infoOptions.find((o) => o.value === selectedInfo.value)?.label ??
|
||||
infoOptions[0].label
|
||||
);
|
||||
});
|
||||
|
||||
// 切換
|
||||
const toggleInfo = () => (infoOpen.value = !infoOpen.value);
|
||||
|
||||
// 點選項目
|
||||
const selectInfo = (opt) => {
|
||||
// 切換全域顯示 → 關閉單獨顯示
|
||||
soloSpriteId.value = null;
|
||||
|
||||
selectedInfo.value = opt.value; // 只存 value
|
||||
infoOpen.value = false;
|
||||
emit("change", opt.value);
|
||||
};
|
||||
|
||||
// 點外面關閉 / Esc 關閉(與檔案A相同)
|
||||
const infoTriggerRef = ref(null);
|
||||
const infoPanelRef = ref(null);
|
||||
|
||||
const onClickOutsideInfo = (e) => {
|
||||
const t = e.target;
|
||||
if (!infoTriggerRef.value || !infoPanelRef.value) return;
|
||||
const insideTrigger = infoTriggerRef.value.contains(t);
|
||||
const insidePanel = infoPanelRef.value.contains(t);
|
||||
if (!insideTrigger && !insidePanel) infoOpen.value = false;
|
||||
};
|
||||
|
||||
const onKeydownInfo = (e) => {
|
||||
if (e.key === "Escape") infoOpen.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", onClickOutsideInfo);
|
||||
document.addEventListener("keydown", onKeydownInfo);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", onClickOutsideInfo);
|
||||
document.removeEventListener("keydown", onKeydownInfo);
|
||||
});
|
||||
|
@ -1,115 +0,0 @@
|
||||
import useSelectedFloor from "@/hooks/useSelectedFloor";
|
||||
import { watch, ref, inject } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
// import useSystemShowData from "@/hooks/useSystemShowData";
|
||||
|
||||
function useForgeFloor() {
|
||||
const route = useRoute();
|
||||
const levelList = ref([]);
|
||||
const { selectedFloor } = useSelectedFloor();
|
||||
const { subscribeData } = inject("system_deviceList");
|
||||
|
||||
const forgeViewer = ref(null);
|
||||
const dataVizExtn = ref(null);
|
||||
const updateViewerFloor = (viewer, dataVisualization) => {
|
||||
forgeViewer.value = viewer;
|
||||
dataVizExtn.value = dataVisualization;
|
||||
};
|
||||
|
||||
const findLevels = () => {
|
||||
forgeViewer.value.model.search(
|
||||
"layer",
|
||||
(nodeIds) => {
|
||||
let levels = [];
|
||||
const tree = forgeViewer.value.model.getInstanceTree();
|
||||
for (let i = 0; i < nodeIds.length; i++) {
|
||||
const dbId = nodeIds[i];
|
||||
const name = tree.getNodeName(dbId);
|
||||
if (!name || name.includes("<沒有層級>")) continue;
|
||||
levels.push({
|
||||
guid: dbId,
|
||||
name,
|
||||
dbId,
|
||||
extension: {
|
||||
buildingStory: true,
|
||||
structure: false,
|
||||
computationHeight: 0,
|
||||
groundPlane: false,
|
||||
hasAssociatedViewPlans: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
levels = levels.sort((a, b) => b.elevation - a.elevation);
|
||||
console.log(levels);
|
||||
levelList.value = levels;
|
||||
},
|
||||
(e) => {
|
||||
console.log(e);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
watch(forgeViewer, () => {
|
||||
findLevels();
|
||||
});
|
||||
|
||||
const hideDbIdFn = () => {
|
||||
const tree = forgeViewer.value?.model.getInstanceTree();
|
||||
const allDbIdsStr = Object.keys(tree.nodeAccess.dbIdToIndex);
|
||||
for (var i = 0; i < allDbIdsStr.length; i++) {
|
||||
forgeViewer.value.hide(parseInt(allDbIdsStr[i]));
|
||||
}
|
||||
};
|
||||
|
||||
const { flatSubData } = useSystemShowData();
|
||||
const showDbIdFn = () => {
|
||||
hideDbIdFn();
|
||||
flatSubData.value.forEach((value, index) => {
|
||||
forgeViewer.value.show(value.forge_dbid);
|
||||
});
|
||||
|
||||
forgeViewer.value.impl.invalidate(true);
|
||||
};
|
||||
|
||||
const showLevels = () => {
|
||||
if (forgeViewer.value) {
|
||||
const currentFloorName =
|
||||
selectedFloor.value?.title?.replaceAll(/U/gi, "") || "";
|
||||
|
||||
const level = levelList.value.find(({ name }) =>
|
||||
name.includes(currentFloorName)
|
||||
);
|
||||
console.log(currentFloorName, level);
|
||||
|
||||
if (!level) {
|
||||
forgeViewer.value?.impl.toggleGhosting(true);
|
||||
forgeViewer.value?.fitToView([forgeViewer.value.model.getRootId()]);
|
||||
showDbIdFn();
|
||||
} else {
|
||||
showDbIdFn();
|
||||
// forgeViewer.value.clearSelection();
|
||||
// forgeViewer.value.model.setAllVisibility(0);
|
||||
forgeViewer.value.impl.toggleGhosting(false);
|
||||
// forgeViewer.value.impl.toggleGroundShadow(false);
|
||||
forgeViewer.value.show(level.dbId);
|
||||
forgeViewer.value.impl.invalidate(true);
|
||||
forgeViewer.value.fitToView([level.dbId]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
(newValue) => {
|
||||
console.log(newValue);
|
||||
newValue && showLevels();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
return { findLevels, showLevels, updateViewerFloor };
|
||||
}
|
||||
|
||||
export default useForgeFloor;
|
@ -1,141 +0,0 @@
|
||||
import { watch, inject, markRaw, ref, computed, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import useHeatmapBarStore from "@/stores/useHeatmapBarStore";
|
||||
// import useSystemShowData from "@/hooks/useSystemShowData";
|
||||
|
||||
export default function useForgeHeatmap() {
|
||||
const route = useRoute();
|
||||
const { subscribeData, realtimeData } = inject("system_deviceList");
|
||||
|
||||
const store = useHeatmapBarStore();
|
||||
|
||||
const forgeViewer = ref(null);
|
||||
const dataVizExtn = ref(null);
|
||||
const updateViewExtension = (viewer, dataVisualization) => {
|
||||
forgeViewer.value = viewer;
|
||||
dataVizExtn.value = dataVisualization;
|
||||
};
|
||||
|
||||
//create the heatmap
|
||||
function getSensorValue(device, sensorType, pointData) {
|
||||
const dev = realtimeData.value.find(
|
||||
({ device_number }) => device_number === device.id
|
||||
);
|
||||
if (dev) {
|
||||
const [min, max] = store.heatmapConfig?.range;
|
||||
const point = dev.data.find(({ point }) => point === route.query?.gas);
|
||||
console.log(9, device, dev, point, (point?.value - min || 0) / max);
|
||||
|
||||
return Math.random();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { flatSubData } = useSystemShowData();
|
||||
|
||||
const data = computed(() =>
|
||||
flatSubData.value?.map((d) => {
|
||||
const pointsMap = d.points ? Object.fromEntries(d.points.map(({ point, value }) => [point, 0])) : {};
|
||||
return {
|
||||
...d,
|
||||
...pointsMap,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
watch(
|
||||
() => realtimeData,
|
||||
() => {
|
||||
dataVizExtn.value &&
|
||||
Object.keys(dataVizExtn.value?.surfaceShading)?.length &&
|
||||
dataVizExtn.value.updateSurfaceShading(getSensorValue);
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const createHeatMap = async () => {
|
||||
if (route.query?.gas === "all" || !route.query?.gas || !dataVizExtn.value) return;
|
||||
const heatMapName = `iot_heatmap_${route.query?.gas}`;
|
||||
console.log("createHeatMap", heatMapName);
|
||||
const {
|
||||
SurfaceShadingData,
|
||||
SurfaceShadingPoint,
|
||||
SurfaceShadingNode,
|
||||
SurfaceShadingGroup,
|
||||
} = Autodesk.DataVisualization.Core;
|
||||
const shadingGroup = new SurfaceShadingGroup(`${heatMapName}`);
|
||||
const rooms = new Map();
|
||||
|
||||
const roomSet = new Set(data.value.filter(({ device_coordinate_3d }) => device_coordinate_3d).map(({ room_dbid }) => room_dbid));
|
||||
// 每個room是一個node
|
||||
[...roomSet].forEach((roomDbId) => {
|
||||
if (!roomDbId) {
|
||||
return;
|
||||
}
|
||||
const room = new SurfaceShadingNode(`room_${roomDbId}`, roomDbId);
|
||||
|
||||
//相同room內的設備
|
||||
data.value
|
||||
.filter(({ room_dbid }) => room_dbid === roomDbId)
|
||||
.forEach(
|
||||
({
|
||||
device_number: id,
|
||||
device_coordinate_3d: position,
|
||||
sensorTypes,
|
||||
}) =>
|
||||
room.addPoint(new SurfaceShadingPoint(id, position, sensorTypes))
|
||||
);
|
||||
|
||||
shadingGroup.addChild(room);
|
||||
});
|
||||
|
||||
// data.value.forEach(
|
||||
// ({
|
||||
// device_number: id,
|
||||
// room_dbid: roomDbId,
|
||||
// device_coordinate_3d: position,
|
||||
// sensorTypes,
|
||||
// }) => {
|
||||
// if (!id || roomDbId == -1 || !roomDbId) {
|
||||
// return;
|
||||
// }
|
||||
// if (!rooms.has(roomDbId)) {
|
||||
// const room = new SurfaceShadingNode(id, roomDbId);
|
||||
// shadingGroup.addChild(room);
|
||||
// rooms.set(roomDbId, room);
|
||||
// }
|
||||
// const room = rooms.get(roomDbId);
|
||||
// room.addPoint(new SurfaceShadingPoint(id, position, route.query.gas));
|
||||
// }
|
||||
// );
|
||||
const shadingData = new SurfaceShadingData(`${heatMapName}`);
|
||||
shadingData.addChild(shadingGroup);
|
||||
shadingData.initialize(forgeViewer.value?.model);
|
||||
await dataVizExtn.value.setupSurfaceShading(
|
||||
forgeViewer.value.model,
|
||||
shadingData
|
||||
);
|
||||
dataVizExtn.value.registerSurfaceShadingColors(
|
||||
route.query?.gas,
|
||||
store.heatmapConfig?.color
|
||||
);
|
||||
dataVizExtn.value.renderSurfaceShading(
|
||||
heatMapName,
|
||||
route.query?.gas,
|
||||
getSensorValue
|
||||
);
|
||||
};
|
||||
|
||||
watch(
|
||||
data,
|
||||
(newValue, oldValue) => {
|
||||
dataVizExtn.value?.removeSurfaceShading();
|
||||
createHeatMap(route.query.gas);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
return { createHeatMap, updateViewExtension };
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
// src/hooks/forge/useForgeSprite.js
|
||||
import { ref, markRaw } from "vue";
|
||||
|
||||
// 裡寫死你的設備資料(可先放幾個測試點)
|
||||
// 寫死設備資料(可先放幾個測試點)
|
||||
// forge_dbid:模型元件 dbId(用於 fitToView / show)
|
||||
// spriteDbId:sprite 的識別 id(要唯一)
|
||||
// device_coordinate_3d:點在模型座標中的位置(務必是模型的 x,y,z 座標系)
|
||||
@ -130,7 +129,7 @@ export default function useForgeSprite() {
|
||||
};
|
||||
};
|
||||
|
||||
// --- 小工具:平移相機到某個世界座標,但保留目前縮放(相機與目標的距離不變)
|
||||
// --- 平移相機到某個世界座標,但保留目前縮放(相機與目標的距離不變)
|
||||
function panToWorldPointKeepZoom(viewer, targetWorld) {
|
||||
const THREE = viewer.impl.THREE || ensureTHREE();
|
||||
const nav = viewer.navigation;
|
||||
@ -151,7 +150,7 @@ export default function useForgeSprite() {
|
||||
viewer.impl.invalidate(true, true, true);
|
||||
}
|
||||
|
||||
// --- 小工具:沿視線方向微量「後退/前進」,不改變目標點
|
||||
// --- 視線方向微量「後退/前進」,不改變目標點
|
||||
function dollyAlongView(viewer, distance = 0) {
|
||||
if (!distance) return;
|
||||
const THREE = viewer.impl.THREE || ensureTHREE();
|
||||
@ -168,7 +167,6 @@ export default function useForgeSprite() {
|
||||
viewer.impl.invalidate(true, true, true);
|
||||
}
|
||||
|
||||
// --- 改寫:保留縮放版 cardfitToView ---
|
||||
const cardfitToView = async ({ forge_dbid, spriteDbId, back = 0 }) => {
|
||||
const viewer = forgeViewer.value;
|
||||
if (!viewer) return;
|
||||
@ -176,7 +174,7 @@ export default function useForgeSprite() {
|
||||
const THREE = viewer.impl.THREE || ensureTHREE();
|
||||
const nav = viewer.navigation;
|
||||
|
||||
// 1) 找到該 sprite 的世界座標(從 STATIC_SUB_DEVICES 取)
|
||||
// 找到該 sprite 的世界座標(從 STATIC_SUB_DEVICES 取)
|
||||
const d = STATIC_SUB_DEVICES.find(
|
||||
(x) => x.spriteDbId === spriteDbId || x.forge_dbid === forge_dbid
|
||||
);
|
||||
@ -188,16 +186,16 @@ export default function useForgeSprite() {
|
||||
d.device_coordinate_3d.z
|
||||
);
|
||||
|
||||
// 2) 僅平移到目標點,保留目前縮放
|
||||
// 僅平移到目標點,保留目前縮放
|
||||
panToWorldPointKeepZoom(viewer, p);
|
||||
|
||||
// 3) 若需要微退/前進(back > 0 代表退後一點,單位與模型座標一致)
|
||||
// 若需要微退/前進(back > 0 代表退後一點,單位與模型座標一致)
|
||||
if (back) {
|
||||
// 注意:若你以前的 back 是「正數=退後」,下面這行就符合直覺。
|
||||
dollyAlongView(viewer, back);
|
||||
}
|
||||
|
||||
// 4) 若有縮放樣式控制(避免被放大/縮小),在這裡把上一次點到的 sprite 還原
|
||||
// 若有縮放樣式控制(避免被放大/縮小),在這裡把上一次點到的 sprite 還原
|
||||
if (lastClickedDbId && lastClickedDbId !== spriteDbId) {
|
||||
try {
|
||||
dataVizExtn.value?.invalidateViewables?.([lastClickedDbId], () => ({
|
||||
@ -210,7 +208,7 @@ export default function useForgeSprite() {
|
||||
lastClickedDbId = spriteDbId;
|
||||
};
|
||||
|
||||
// 只顯示寫死資料對應的 dbId(效能快、可見度好)
|
||||
// 只顯示寫死資料對應的 dbId
|
||||
const isolateSubSystemObjects = () => {
|
||||
const ids = STATIC_SUB_DEVICES.map((d) => d.forge_dbid).filter(Boolean);
|
||||
if (ids.length) {
|
||||
@ -235,7 +233,7 @@ export default function useForgeSprite() {
|
||||
cardfitToView,
|
||||
isolateSubSystemObjects,
|
||||
clear,
|
||||
// 給你動態調整測試資料用(可選)
|
||||
// 動態調整測試資料用(可選)
|
||||
__staticDevices: STATIC_SUB_DEVICES,
|
||||
};
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
|
||||
function useSelectedFloor() {
|
||||
const { currentFloor } = inject("system_deviceList");
|
||||
const route = useRoute();
|
||||
const selectedFloor = computed(() =>
|
||||
currentFloor.value?.find(({ key }) => key == route.params.floor_id)
|
||||
);
|
||||
|
||||
return {
|
||||
selectedFloor,
|
||||
currentFloor,
|
||||
};
|
||||
}
|
||||
|
||||
export default useSelectedFloor;
|
@ -44,7 +44,7 @@
|
||||
class="absolute top-16 left-0 z-50 w-64 rounded-xl border border-gray-100 bg-white shadow-lg p-2"
|
||||
@click.stop
|
||||
>
|
||||
<ul class="max-h-48 overflow-y-auto">
|
||||
<ul class="max-h-48 overflow-y-auto text-brand-black">
|
||||
<li
|
||||
v-for="item in facilities"
|
||||
:key="item"
|
||||
|
@ -93,7 +93,7 @@
|
||||
<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">
|
||||
<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>
|
||||
@ -125,7 +125,7 @@
|
||||
<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">
|
||||
<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>
|
||||
@ -159,7 +159,7 @@
|
||||
<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">
|
||||
<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>
|
||||
|
@ -93,7 +93,7 @@
|
||||
<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">
|
||||
<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>
|
||||
@ -125,7 +125,7 @@
|
||||
<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">
|
||||
<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>
|
||||
@ -159,7 +159,7 @@
|
||||
<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">
|
||||
<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>
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
import axios from "axios";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
|
||||
const useHeatmapBarStore = defineStore("heatmap", () => {
|
||||
const route = useRoute();
|
||||
|
||||
const allHeatMaps = ref({});
|
||||
const heatmapConfig = computed(() => allHeatMaps.value[route.query?.gas]);
|
||||
|
||||
const getConfig = async () => {
|
||||
const api =
|
||||
import.meta.env.MODE === "production"
|
||||
? "/dist/config.json"
|
||||
: "/config.json";
|
||||
const res = await axios.get(api);
|
||||
console.log(res);
|
||||
allHeatMaps.value = res.data.heatmap;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getConfig();
|
||||
});
|
||||
|
||||
const heat_bar_isShow = computed(() => Boolean(heatmapConfig.value));
|
||||
|
||||
return { heatmapConfig, heat_bar_isShow };
|
||||
});
|
||||
|
||||
export default useHeatmapBarStore;
|
@ -1,5 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: false, // 關閉深色模式
|
||||
darkMode: "class",
|
||||
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
container: {
|
||||
@ -72,5 +74,6 @@ module.exports = {
|
||||
},
|
||||
"light",
|
||||
],
|
||||
base: false,
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user