diff --git a/index.html b/index.html
index 2b7c391..05d6236 100644
--- a/index.html
+++ b/index.html
@@ -11,7 +11,7 @@
新創賦能
-
+
{
+ const name =
+ (instance && (instance.type?.name || instance.type?.__file)) ||
+ "(anonymous component)";
+ // 在 patch/update 階段若報 instance.update is not a function,哪顆元件
+ console.error("[VueError]", name, info, err);
+};
+
+// (選用)開啟 devtools
+// app.config.devtools = true;
+
+// ===========================================
+// 插件與全域元件
+// ===========================================
app.use(createPinia());
app.use(router);
app.use(Antd);
app.use(i18n);
-// 组装成一个对象
+// 全域元件註冊(維持你的寫法)
const allGlobalComponents = { SvgIcon, FontAwesomeIcon };
-const globalComponent = {
+app.use({
install(app) {
- // 循环注册所有的全局组件
- Object.keys(allGlobalComponents).forEach((componentName) => {
- app.component(componentName, allGlobalComponents[componentName]);
+ Object.keys(allGlobalComponents).forEach((k) => {
+ app.component(k, allGlobalComponents[k]);
});
},
-};
-app.use(globalComponent);
+});
+
+// 指令
app.use(focusPlugin);
app.use(draggable);
+
+// ===========================================
+// Mount(保證只呼叫一次)
+// ===========================================
app.mount("#app");
diff --git a/src/router/index.js b/src/router/index.js
index 49db595..fcebba6 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -56,6 +56,12 @@ const router = createRouter({
name: "assetManagement",
component: () => import("@/views/AssetManagement/AssetManagement.vue"),
},
+ {
+ path: "/rtsp",
+ name: "rtsp",
+ component: () => import("@/views/rtsp/Rtsp.vue"),
+ meta: { layout: "map", title: "rtsp" },
+ },
{
path: "/alert",
name: "alert",
diff --git a/src/views/AssetManagement/components/AssetMainList.vue b/src/views/AssetManagement/components/AssetMainList.vue
index f0068c7..ea3fef0 100644
--- a/src/views/AssetManagement/components/AssetMainList.vue
+++ b/src/views/AssetManagement/components/AssetMainList.vue
@@ -91,12 +91,12 @@ watch(selectedBtn, (newValue) => {
:getData="getMainSystems"
:formState="formState"
/>
-
+
{
-
+
{
+ try {
+ console.log(
+ `[AlertActionItem] ${label}:`,
+ JSON.parse(JSON.stringify(payload))
+ );
+ } catch (e) {
+ console.log(`[AlertActionItem] ${label}:`, payload);
+ }
+};
+
+// 初始就印出整個 props 與 editRecord 內容
+debugLog("props", props);
+debugLog("initial props.editRecord", props?.editRecord);
+
+onMounted(() => {
+ debugLog("onMounted props", props);
+ debugLog("onMounted props.editRecord", props?.editRecord);
+});
+
const form = ref(null);
const dateItem = ref([
@@ -64,18 +93,43 @@ const updateFileList = (files) => {
formState.value.lorf = files;
};
+// ---------------------- 告警影片儲存位置:顯示 API 的 video_url ----------------------
+// 重要:你的 會從 value[name] 取值
+// 所以我們把 videoLocation 寫成 { videoLocation: string } 的物件
+const videoLocation = ref({ videoLocation: "" });
+const showTooltip = ref(false);
+
+async function copyToClipboard() {
+ const text = videoLocation.value.videoLocation || "";
+ try {
+ await navigator.clipboard.writeText(text);
+ // 觸發 tooltip 動畫
+ showTooltip.value = false;
+ await nextTick();
+ showTooltip.value = true;
+ setTimeout(() => {
+ showTooltip.value = false;
+ }, 1500);
+ } catch (err) {
+ console.error("複製失敗:", err);
+ }
+}
+
const onOk = async () => {
const formData = new FormData(form.value);
formData.delete("oriFile");
- formState.value?.lorf.forEach((file, index) => {
- formData.append(`lorf[${index}].id`, file.id ? file.id : "");
- formData.append(`lorf[${index}].file`, file.id ? null : file);
+ (formState.value?.lorf ?? []).forEach((file, index) => {
+ formData.append(`lorf[${index}].id`, file?.id ? file.id : "");
+ // 只有新檔案才上傳檔案本體
+ if (!file?.id && file) {
+ formData.append(`lorf[${index}].file`, file);
+ }
formData.append(
`lorf[${index}].save_file_name`,
- file.id ? file.save_file_name : ""
+ file?.id ? file.save_file_name : ""
);
- formData.append(`lorf[${index}].ori_file_name`, file.name);
+ formData.append(`lorf[${index}].ori_file_name`, file?.name ?? "");
});
formData.append(
@@ -83,9 +137,9 @@ const onOk = async () => {
dayjs(dateItem.value[0].value).format("YYYY-MM-DD")
);
- props.editRecord.id && formData.append("id", props.editRecord.id);
-
- props.editRecord.uuid && formData.append("error_code", props.editRecord.uuid);
+ if (props.editRecord.id) formData.append("id", props.editRecord.id);
+ if (props.editRecord.uuid)
+ formData.append("error_code", props.editRecord.uuid);
formData.append("work_type", 2);
formData.append(
@@ -96,7 +150,7 @@ const onOk = async () => {
);
try {
- const value = await handleSubmit(alertSchema, formState.value);
+ await handleSubmit(alertSchema, formState.value);
const res = await postOperationRecord(formData);
if (res.isSuccess) {
search?.();
@@ -123,32 +177,36 @@ const onCancel = () => {
description: "",
lorf: [],
};
+ // 重置顯示用的影片路徑
+ videoLocation.value.videoLocation = "";
handleErrorReset();
updateEditRecord?.(null);
alert_action_item.close();
};
+// 同步 props.editRecord -> formState / 日期 / 維修項目 / 影片網址
watch(
() => props.editRecord,
(newVal) => {
+ debugLog("watch props.editRecord changed", newVal);
if (newVal) {
for (let [key, value] of Object.entries(newVal)) {
- // 僅更新 formState 中已有的屬性
if (key in formState.value) {
formState.value[key] = value;
}
- // 處理 start_time
if (key === "start_time") {
- formState.value.start_time = value
- ? dayjs(value).format("YYYY-MM-DD")
- : dayjs().format("YYYY-MM-DD");
- dateItem.value[0].value = value;
+ const d = value ? dayjs(value) : dayjs();
+ formState.value.start_time = d.format("YYYY-MM-DD");
+ dateItem.value[0].value = d; // 一律用 dayjs 物件
}
- // 維修項目
if (key === "full_name") {
- formState.value.fix_do = value;
+ formState.value.fix_do = value ?? "";
}
}
+ // 取 API 回傳的影片位址(與 device_number 同來源物件)
+ videoLocation.value.videoLocation =
+ newVal?.video_url ?? newVal?.videoUrl ?? newVal?.video_path ?? "";
+ debugLog("derived videoLocation", videoLocation.value.videoLocation);
}
},
{ immediate: true }
@@ -163,7 +221,11 @@ watch(
width="710"
>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t("alert.copied") }}
+
+
+
+
+
+
+
diff --git a/src/views/dashboard/components/DashboardEffectScatter.vue b/src/views/dashboard/components/DashboardEffectScatter.vue
index 1425032..dc3a4f6 100644
--- a/src/views/dashboard/components/DashboardEffectScatter.vue
+++ b/src/views/dashboard/components/DashboardEffectScatter.vue
@@ -4,6 +4,7 @@ import EffectScatter from "@/components/chart/EffectScatter.vue";
import DashboardEffectScatterModal from "./DashboardEffectScatterModal.vue";
import useSearchParam from "@/hooks/useSearchParam";
import { computed, inject, ref, watch } from "vue";
+import { twMerge } from "tailwind-merge";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
@@ -91,45 +92,47 @@ const handleItemClick = (params) => {
watch(
[searchParams, () => asset_floor_chart.value, () => props.data],
([newValue, newChart, newData], [oldValue]) => {
- console.groupCollapsed("[FloorMap] watch fired");
- console.log("floor_id =", newValue.floor_id);
- console.log("chart ready =", !!newChart);
- console.log("data keys =", Object.keys(newData || {}).length);
- console.groupEnd();
- const floorId = newValue.floor_id;
- const chartReady = !!newChart;
- // (1) 首次/樓層切換:不等 data,先載 SVG
- if (floorId && chartReady && currentFloorId.value !== floorId) {
- console.log("[FloorMap] load SVG for floor:", floorId);
- currentFloorId.value = floorId;
- newChart.updateSvg(
- {
- full_name: floorId,
- path: `${FILE_BASEURL}/upload/floor_map/${floorId}.svg`,
- },
- defaultOption(floorId, []) // 先不帶點位
- );
- setTimeout(() => {
- if (newChart.chart) {
- newChart.chart.off("click");
- newChart.chart.on("click", handleItemClick);
- }
- }, 100);
- }
-
- // (2) 資料到齊後,再補點位(不重載 SVG)
if (
- floorId &&
- chartReady &&
- Object.keys(newData || {}).length > 0 &&
- newChart.chart
+ newValue.floor_id &&
+ newChart &&
+ Object.keys(newData || {}).length > 0
) {
- console.log("[FloorMap] update series only for floor:", floorId);
- newChart.chart.setOption(
- { series: defaultOption(floorId, currentIconData.value).series },
- false,
- true
- );
+ const isFloorChanged = currentFloorId.value !== newValue.floor_id;
+
+ if (isFloorChanged) {
+ // 樓層切換時才重新載入 SVG
+ console.log(
+ "Floor changed, updating chart with new SVG",
+ newValue.floor_id
+ );
+ currentFloorId.value = newValue.floor_id;
+ newChart.updateSvg(
+ {
+ full_name: newValue.floor_id,
+ path: `${FILE_BASEURL}/upload/floor_map/${newValue.floor_id}.svg`,
+ },
+ defaultOption(newValue.floor_id, currentIconData.value)
+ );
+
+ // 添加點擊事件監聽
+ setTimeout(() => {
+ if (newChart.chart) {
+ newChart.chart.off("click"); // 移除舊的監聽器
+ newChart.chart.on("click", handleItemClick);
+ }
+ }, 100);
+ } else if (currentFloorId.value === newValue.floor_id && newChart.chart) {
+ // 只是資料更新時,只更新圖表資料,不重新載入 SVG
+ console.log("Data updated, refreshing chart data only");
+ newChart.chart.setOption(
+ {
+ series: defaultOption(newValue.floor_id, currentIconData.value)
+ .series,
+ },
+ false,
+ true
+ );
+ }
}
},
{
diff --git a/src/views/dashboard/components/DashboardEffectScatterModal.vue b/src/views/dashboard/components/DashboardEffectScatterModal.vue
index b2b6278..453ea78 100644
--- a/src/views/dashboard/components/DashboardEffectScatterModal.vue
+++ b/src/views/dashboard/components/DashboardEffectScatterModal.vue
@@ -6,10 +6,7 @@ import { useI18n } from "vue-i18n";
import { twMerge } from "tailwind-merge";
const { t } = useI18n();
-const currentTab = ref("desktop");
-const changeOpenKey = (key) => {
- currentTab.value = key;
-};
+
const props = defineProps({
data: {
type: Object,
@@ -17,6 +14,16 @@ const props = defineProps({
},
});
+const isRtsp = computed(() => props.data?.is_rtsp === true);
+
+const monitorUrl = computed(() => props.data?.rtsp_url || "");
+
+// ------ tab 狀態 / 控制(非 RTSP 時使用)------
+const currentTab = ref("desktop");
+const changeOpenKey = (key) => {
+ currentTab.value = key;
+};
+
const modal = ref(null);
// 當 data 有值時自動開啟 modal
@@ -24,15 +31,23 @@ watch(
() => props.data,
(newData) => {
if (newData) {
+ console.log("[props.data] =\n", JSON.stringify(props.data, null, 2));
dashboard_effectScatter_modal.showModal();
+ if (!isRtsp.value) currentTab.value = "desktop";
+ console.debug(
+ "[Modal Debug] is_rtsp:",
+ newData.is_rtsp,
+ "monitorUrl:",
+ monitorUrl.value
+ );
}
},
{ immediate: true }
);
-// 關閉 modal 的處理函數
+// 關閉 modal
const handleCancel = () => {
- currentTab.value = "desktop"; // 重置到 desktop tab
+ currentTab.value = "desktop";
dashboard_effectScatter_modal.close();
};
@@ -47,70 +62,75 @@ const handleCancel = () => {
:draggable="true"
modalClass="max-h-[80vh]"
>
+
{{ props.data?.full_name }}
+
-
-
-
-
+
+
+
+
+
+
+