fix: 修正文字顏色錯誤套用瀏覽器 darkMode 問題

This commit is contained in:
MJM_2025_05\polly 2025-08-22 13:05:10 +08:00
parent c4d78bd5b6
commit cb74fdb0f2
15 changed files with 209 additions and 632 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,4 +1,3 @@
<!-- src/App.vue -->
<template>
<section id="app" class="flex flex-col min-h-screen">
<NavBar />

View File

@ -1,2 +0,0 @@
export const GET_FORGETOKEN_API = `/api/forge/oauth/token`;
export const GET_FORGEURN_API = `/api/Device/GetBuild`

View File

@ -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 });
}
// 取得 TokenPromise 版)
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.");
}
}

View File

@ -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);
}

View File

@ -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);
}
// spritessprites 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 idnull /
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 idnull /
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);
});

View File

@ -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;

View File

@ -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 };
}

View File

@ -1,7 +1,6 @@
// src/hooks/forge/useForgeSprite.js
import { ref, markRaw } from "vue";
// 寫死你的設備資料(可先放幾個測試點)
// 寫死設備資料(可先放幾個測試點)
// forge_dbid模型元件 dbId用於 fitToView / show
// spriteDbIdsprite 的識別 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,
};
}

View File

@ -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;

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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,
},
};