|
@@ -0,0 +1,582 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="content">
|
|
|
|
|
+ <div class="viewer-area">
|
|
|
|
|
+ <MapCadViewer
|
|
|
|
|
+ ref="viewerRef"
|
|
|
|
|
+ :map-config="mapConfig"
|
|
|
|
|
+ :start-in-cad-mode="START_IN_CAD_MODE"
|
|
|
|
|
+ @mode-change="onModeChange"
|
|
|
|
|
+ @feature-select="onFeatureSelect"
|
|
|
|
|
+ @marker-click="onMarkerClick"
|
|
|
|
|
+ @marker-open-cad="onMarkerOpenCad"
|
|
|
|
|
+ @icon-placed="onIconPlaced"
|
|
|
|
|
+ @icon-moved="onIconMoved"
|
|
|
|
|
+ @device-click="onDeviceClick"
|
|
|
|
|
+ @device-popup-close="onDevicePopupClose"
|
|
|
|
|
+ @icon-deleted="onIconDeleted"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div v-if="viewerReady" class="coord-bar">
|
|
|
|
|
+ <span>X: {{ currentCoord.x.toFixed(2) }}</span>
|
|
|
|
|
+ <span>Y: {{ currentCoord.y.toFixed(2) }}</span>
|
|
|
|
|
+ <span v-if="placingMode" class="placing-hint">| 放置: {{ placingDevice?.strname }} (Esc取消)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="device-panel" :class="{ collapsed: panelCollapsed }">
|
|
|
|
|
+ <div class="panel-header" @click="panelCollapsed = !panelCollapsed">
|
|
|
|
|
+ <span class="panel-title">设备列表</span>
|
|
|
|
|
+ <span class="panel-toggle">{{ panelCollapsed ? '展开' : '收起' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-show="!panelCollapsed" class="panel-body">
|
|
|
|
|
+ <a-table :columns="columns" :data-source="devices" :pagination="false" row-key="id" size="small" :scroll="{ y: 360 }">
|
|
|
|
|
+ <template #bodyCell="{ column, record }">
|
|
|
|
|
+ <template v-if="column.key === 'netStatus'">
|
|
|
|
|
+ <a-tag :color="statusColor(record.netStatus)">{{ statusText(record.netStatus) }}</a-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template v-if="column.key === 'action'">
|
|
|
|
|
+ <a-button v-if="!isDevicePlaced(record)" type="primary" size="small" @click="enterPlacingMode(record)">布点</a-button>
|
|
|
|
|
+ <a-button v-else type="link" size="small" danger @click="revokePlacement(record)">撤销</a-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 加载蒙版DOM -->
|
|
|
|
|
+ <div v-if="loading" class="loading-mask">
|
|
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
|
|
+ <div class="loading-text">正在下载图纸数据,请稍后...</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+ import { ref, onMounted, onUnmounted, onBeforeUnmount, computed, toRef, reactive, watch } from 'vue';
|
|
|
|
|
+ import { MapCadViewer } from '/@/components/map/map-cad-viewer.es';
|
|
|
|
|
+ import { saveOrUpdate } from '../deviceManager/deviceTable/device.api';
|
|
|
|
|
+ import type { CadData, MapConfig } from '/@/components/map/types/index.d';
|
|
|
|
|
+
|
|
|
|
|
+ import '/@/components/map/map-cad-viewer.css';
|
|
|
|
|
+import { getToken } from '/@/utils/auth';
|
|
|
|
|
+
|
|
|
|
|
+import { useGlobSetting } from '/@/hooks/setting';
|
|
|
|
|
+ const loading = ref<boolean>(false);
|
|
|
|
|
+const globSetting = useGlobSetting();
|
|
|
|
|
+const baseApiUrl = globSetting.domainUrl;
|
|
|
|
|
+ const viewerReady = ref(false);
|
|
|
|
|
+ const panelCollapsed = ref(true);
|
|
|
|
|
+
|
|
|
|
|
+ const placingMode = ref(false);
|
|
|
|
|
+ const placingDevice = ref<DeviceRecord | null>(null);
|
|
|
|
|
+ const currentCoord = reactive({ x: 0, y: 0, z: 0 });
|
|
|
|
|
+ const columns = [
|
|
|
|
|
+ { title: '设备名称', dataIndex: 'strname', ellipsis: true, align: 'center' },
|
|
|
|
|
+ { title: '状态', key: 'netStatus', width: 60, align: 'center' },
|
|
|
|
|
+ { title: '操作', key: 'action', width: 60, align: 'center' },
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ /** 已布点设备 ID 集合(保证列表按钮状态可响应更新) */
|
|
|
|
|
+ const placedDeviceIds = ref<Set<string>>(new Set());
|
|
|
|
|
+
|
|
|
|
|
+ const props = defineProps({
|
|
|
|
|
+ devices: {
|
|
|
|
|
+ type: Array,
|
|
|
|
|
+ default: () => [],
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ const deviceList = toRef(props, 'devices');
|
|
|
|
|
+
|
|
|
|
|
+ /** 持久化的布点记录 */
|
|
|
|
|
+ interface PersistedMarker {
|
|
|
|
|
+ deviceId: string;
|
|
|
|
|
+ x: number;
|
|
|
|
|
+ y: number;
|
|
|
|
|
+ }
|
|
|
|
|
+ /** color */
|
|
|
|
|
+ interface colorInfo {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ color: number[];
|
|
|
|
|
+ }
|
|
|
|
|
+ interface markerInfo {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ type: string;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ coords: number[];
|
|
|
|
|
+ }
|
|
|
|
|
+ function statusColor(s: string) {
|
|
|
|
|
+ return { 1: 'green', 0: 'gray' }[s] || 'default';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function statusText(s: string) {
|
|
|
|
|
+ return { 1: '在线', 0: '离线' }[s] || s;
|
|
|
|
|
+ }
|
|
|
|
|
+ /** 判断设备是否已完成布点 */
|
|
|
|
|
+ function isDevicePlaced(device: DeviceRecord) {
|
|
|
|
|
+ const id = getDeviceId(device);
|
|
|
|
|
+ return !!device.markerId || (id !== '' && placedDeviceIds.value.has(id));
|
|
|
|
|
+ }
|
|
|
|
|
+ /** 获取设备唯一标识 */
|
|
|
|
|
+ function getDeviceId(device: DeviceRecord) {
|
|
|
|
|
+ return String(device.deviceID ?? device.id ?? '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 根据 deviceId 查找设备记录 */
|
|
|
|
|
+ function findDevice(deviceId: string) {
|
|
|
|
|
+ return deviceList.value.find((item) => getDeviceId(item as DeviceRecord) === deviceId) as DeviceRecord | undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 判断设备是否处于报警 */
|
|
|
|
|
+ function isDeviceAlarm(device?: DeviceRecord | null) {
|
|
|
|
|
+ return !!device?.syswarnLevel;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 根据报警状态获取布点颜色 */
|
|
|
|
|
+ function getMarkerColor(device?: DeviceRecord | null) {
|
|
|
|
|
+ return isDeviceAlarm(device) ? MARKER_COLOR_ALARM : MARKER_COLOR_NORMAL;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 正常状态布点颜色(绿色) */
|
|
|
|
|
+ const MARKER_COLOR_NORMAL = [15, 23, 250];
|
|
|
|
|
+ /** 报警状态布点颜色(红色) */
|
|
|
|
|
+ const MARKER_COLOR_ALARM = [215, 23, 25];
|
|
|
|
|
+
|
|
|
|
|
+ /** 将 markersMap 中的布点状态同步到设备列表(列表刷新后仍显示「已布点」) */
|
|
|
|
|
+ function syncPlacedStateToDevices() {
|
|
|
|
|
+ const list: PersistedMarker[] = [];
|
|
|
|
|
+ markersMap.forEach((m) => {
|
|
|
|
|
+ list.push({ id: m.id, x: m.worldPos.x, y: m.worldPos.y });
|
|
|
|
|
+ });
|
|
|
|
|
+ const colors: colorInfo[] = [];
|
|
|
|
|
+ if (markersMap.size > 0) {
|
|
|
|
|
+ markersMap.forEach((m) => {
|
|
|
|
|
+ colors.push({ id: '' + m.id, color: getMarkerColor(findDevice(m.id)) });
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ deviceList.value.forEach((item: any) => colors.push({ id: '' + item.deviceID, color: getMarkerColor(findDevice(item.deviceID)) }));
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log(colors);
|
|
|
|
|
+ viewerRef.value?.setIconsColor(colors);
|
|
|
|
|
+ if (currentDeviceIcon.value && viewerRef.value) {
|
|
|
|
|
+ const dev = findDevice(currentDeviceIcon.value.id);
|
|
|
|
|
+ viewerRef.value.showDevicePopup(currentDeviceIcon.value.id, dev.strinstallpos, buildDevicePopupHtml(dev, currentDeviceIcon.value.type));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!markersDraw.value) {
|
|
|
|
|
+ let devs: markerInfo[] = [];
|
|
|
|
|
+ const next = new Set(placedDeviceIds.value);
|
|
|
|
|
+ deviceList.value.forEach((dev: any) => {
|
|
|
|
|
+ if (dev.other3 && dev.other3 == 1) {
|
|
|
|
|
+ next.add(dev.deviceID);
|
|
|
|
|
+ devs.push({ id: dev.deviceID, type: 'duaSpeCamera', coords: [parseFloat(dev.other1), parseFloat(dev.other2)], name: '双光谱摄像机' });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ placedDeviceIds.value = next;
|
|
|
|
|
+ markersDraw.value = true;
|
|
|
|
|
+ viewerRef.value?.loadIcons(devs);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 页面加载时先恢复「已布点」按钮状态(无需等待图纸加载)
|
|
|
|
|
+ // placedDeviceIds.value = new Set(loadPersistedMarkers().map((m) => String(m.deviceId)));
|
|
|
|
|
+
|
|
|
|
|
+ function enterPlacingMode(device: DeviceRecord) {
|
|
|
|
|
+ if (placingDevice.value && placingDevice.value.deviceID == device.deviceID) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ placingMode.value = true;
|
|
|
|
|
+ placingDevice.value = device;
|
|
|
|
|
+ console.log(device);
|
|
|
|
|
+ viewerRef.value?.toggleDrawingMode('duaSpeCamera', '' + device.deviceID);
|
|
|
|
|
+ }
|
|
|
|
|
+ /** 撤销指定设备的布点 */
|
|
|
|
|
+ function revokePlacement(device: DeviceRecord) {
|
|
|
|
|
+ viewerRef.value?.deletePlacedIcon(device.deviceID);
|
|
|
|
|
+ const next = new Set(placedDeviceIds.value);
|
|
|
|
|
+ next.delete(device.deviceID);
|
|
|
|
|
+ placedDeviceIds.value = next;
|
|
|
|
|
+
|
|
|
|
|
+ let dev = { id: device.deviceID, other1: null, other2: null, other3: 0 };
|
|
|
|
|
+ onSubmit(dev);
|
|
|
|
|
+ }
|
|
|
|
|
+ function removeMarker(markerId: string) {
|
|
|
|
|
+ const m = markersMap.get(markerId);
|
|
|
|
|
+ if (!m) return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const viewerRef = ref<InstanceType<typeof MapCadViewer>>();
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 启动配置 =====
|
|
|
|
|
+ /** 启动后默认进入的视图:false=地图页面, true=CAD 图纸页面 */
|
|
|
|
|
+ const START_IN_CAD_MODE = true;
|
|
|
|
|
+
|
|
|
|
|
+ /** 地图配置 */
|
|
|
|
|
+ const mapConfig: Partial<MapConfig> = {
|
|
|
|
|
+ center: [114.0, 38.0],
|
|
|
|
|
+ zoom: 5,
|
|
|
|
|
+ tileUrl: 'http://182.92.126.35:9999/sys/common/static/map/{z}/{x}/{y}.png',
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const cadData = ref<CadData | undefined>();
|
|
|
|
|
+ const currentIconId = ref('duaSpeCamera');
|
|
|
|
|
+ const markersDraw = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+ /** 当前点击的设备图标(用于定时刷新时重建 HTML 内容) */
|
|
|
|
|
+ const currentDeviceIcon = ref<{ id: string; type: string; name?: string } | null>(null);
|
|
|
|
|
+
|
|
|
|
|
+ /** 设备类型 → 中文名称映射(弹框"类型"行显示用) */
|
|
|
|
|
+ const DEVICE_TYPE_NAMES: Record<string, string> = {
|
|
|
|
|
+ gate: '风门',
|
|
|
|
|
+ modelsensor: '传感器',
|
|
|
|
|
+ obfurage: '密闭',
|
|
|
|
|
+ speed: '测风装置',
|
|
|
|
|
+ duaSpeCamera: '双光谱摄像机',
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ /** 设备弹框数据刷新定时器(每 3 秒触发一次) */
|
|
|
|
|
+ let devicePopupTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
+ function onModeChange(mode: string) {
|
|
|
|
|
+ console.log('Viewer mode changed:', mode);
|
|
|
|
|
+ currentMode.value = mode as any;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const currentMode = ref<'map' | 'cad' | 'drawing'>('cad');
|
|
|
|
|
+ const isCadMode = computed(() => currentMode.value === 'cad' || currentMode.value === 'drawing');
|
|
|
|
|
+
|
|
|
|
|
+ /** 点击 CAD 图元(线/面/文字等)回调:输出图元信息到控制台 */
|
|
|
|
|
+ function onFeatureSelect(feature: any) {
|
|
|
|
|
+ console.log('Selected feature:', feature.id, feature.typename, 'layer:', feature.layer);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 点击地图标注点回调:输出标注点信息到控制台 */
|
|
|
|
|
+ function onMarkerClick(marker: any) {
|
|
|
|
|
+ console.log('Marker clicked:', marker.label, 'coords:', marker.coords, 'switchToCad:', marker.switchToCad, 'popup:', marker.popup);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 点击标注点弹框中的 CAD 入口回调
|
|
|
|
|
+ * 携带预先定义好的设备图标数组(演示用)一并传入 CAD 视图,
|
|
|
|
|
+ * 使这些图标按类型和坐标直接绘制到图纸上。
|
|
|
|
|
+ */
|
|
|
|
|
+ function onMarkerOpenCad(marker: any) {
|
|
|
|
|
+ console.log('Marker title clicked, opening CAD:', marker.label);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据设备 id/type 动态拼接弹框的 HTML div(标题、类型、数据值)
|
|
|
|
|
+ * @param icon 设备图标信息(id/type/name)
|
|
|
|
|
+ * @returns HTML 字符串,包含设备ID、类型、以及按类型区分的模拟实时数据行
|
|
|
|
|
+ * 说明:数据值目前为模拟数据(随机数),实际项目中可替换为接口请求结果
|
|
|
|
|
+ */
|
|
|
|
|
+ function buildDevicePopupHtml(device, type: string): string {
|
|
|
|
|
+ // 优先用类型映射得到中文名称,其次用传入 name,最后回退为 type 原文
|
|
|
|
|
+ console.log('buildDevicePopupHtml');
|
|
|
|
|
+ console.log(device);
|
|
|
|
|
+ // const typeName = DEVICE_TYPE_NAMES[icon.type] || icon.name || icon.type;
|
|
|
|
|
+ // 生成 [min, max) 区间随机数,保留 dec 位小数
|
|
|
|
|
+ // const rand = (min: number, max: number, dec = 1) => (min + Math.random() * (max - min)).toFixed(dec);
|
|
|
|
|
+
|
|
|
|
|
+ // 根据不同类型拼接不同的"数据值"行
|
|
|
|
|
+ let dataRows = '';
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case 'gate': // 风门:开关状态 + 风量
|
|
|
|
|
+ dataRows = ``;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'modelsensor': // 传感器:温度 + 湿度
|
|
|
|
|
+ dataRows = ``;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'obfurage': // 密闭:密闭状态 + 压差
|
|
|
|
|
+ dataRows = ``;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'speed': // 测风装置:风速 + 风向
|
|
|
|
|
+ dataRows = ``;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'duaSpeCamera': // 双光谱摄像机通用
|
|
|
|
|
+ dataRows = `
|
|
|
|
|
+ <div class="dev-row"><span class="dev-key">平均温度</span><span class="dev-val">${device.readData.avg}℃</span></div>
|
|
|
|
|
+ <div class="dev-row"><span class="dev-key">最高温度</span><span class="dev-val">${device.readData.max}℃</span></div>
|
|
|
|
|
+ <div class="dev-row"><span class="dev-key">最低温度</span><span class="dev-val">${device.readData.min}℃</span></div>
|
|
|
|
|
+ <div class="dev-row"><span class="dev-key">设备类型</span><span class="dev-val">${device.typeName}</span></div>`;
|
|
|
|
|
+ break;
|
|
|
|
|
+ default: // 其他类型:通用数值
|
|
|
|
|
+ dataRows = `
|
|
|
|
|
+ <div class="dev-row"><span class="dev-key">数值</span><span class="dev-val">${rand(0, 100)}</span></div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 组装完整弹框 HTML:设备ID + 类型 + 数据行
|
|
|
|
|
+ return `
|
|
|
|
|
+ <div class="dev-popup">
|
|
|
|
|
+ ${dataRows}
|
|
|
|
|
+ </div>`;
|
|
|
|
|
+ }
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 点击 CAD 中绘制的设备图标回调
|
|
|
|
|
+ * 1. 保存当前设备(供定时器重建 HTML)
|
|
|
|
|
+ * 2. 立即拼接一次 HTML 并传给图纸弹框显示
|
|
|
|
|
+ * 3. 启动 3 秒定时器,周期性刷新弹框数据(模拟实时数据)
|
|
|
|
|
+ */
|
|
|
|
|
+ function onDeviceClick(icon: { id: string; type: string; name?: string }) {
|
|
|
|
|
+ console.log('Device clicked:', icon);
|
|
|
|
|
+ currentDeviceIcon.value = icon;
|
|
|
|
|
+ // 立即拼接一次并显示
|
|
|
|
|
+ if (currentDeviceIcon.value && viewerRef.value) {
|
|
|
|
|
+ const dev = findDevice(currentDeviceIcon.value.id);
|
|
|
|
|
+ viewerRef.value.showDevicePopup(currentDeviceIcon.value.id, dev.strinstallpos, buildDevicePopupHtml(dev, currentDeviceIcon.value.type));
|
|
|
|
|
+ }
|
|
|
|
|
+ // viewerRef.value?.showDevicePopup(icon.id, icon.type, buildDevicePopupHtml(icon));
|
|
|
|
|
+ // 每 3 秒更新一次新的 div 数据到 CAD 图纸弹框
|
|
|
|
|
+ // if (devicePopupTimer) clearInterval(devicePopupTimer); // 防重复启动
|
|
|
|
|
+ // devicePopupTimer = setInterval(() => {
|
|
|
|
|
+ // if (currentDeviceIcon.value && viewerRef.value) {
|
|
|
|
|
+ // viewerRef.value.showDevicePopup(currentDeviceIcon.value.id, currentDeviceIcon.value.type, buildDevicePopupHtml(currentDeviceIcon.value));
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }, 3000);
|
|
|
|
|
+ }
|
|
|
|
|
+ /** 设备弹框关闭回调:停止刷新定时器并清空当前设备 */
|
|
|
|
|
+ function onDevicePopupClose() {
|
|
|
|
|
+ console.log('Device popup closed, stopping timer');
|
|
|
|
|
+
|
|
|
|
|
+ currentDeviceIcon.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const markersMap = new Map<
|
|
|
|
|
+ string,
|
|
|
|
|
+ {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ worldPos: { x: number; y: number };
|
|
|
|
|
+ iconSprite: string;
|
|
|
|
|
+ }
|
|
|
|
|
+ >();
|
|
|
|
|
+ /** 绘制模式下放置了设备图标回调 */
|
|
|
|
|
+ function onIconPlaced(icon: any) {
|
|
|
|
|
+ console.log('Icon placed:', icon);
|
|
|
|
|
+ viewerRef.value?.toggleDrawingMode('');
|
|
|
|
|
+
|
|
|
|
|
+ markersMap.set(icon.id, {
|
|
|
|
|
+ id: icon.id,
|
|
|
|
|
+ worldPos: { x: icon.coords[0], y: icon.coords[1] },
|
|
|
|
|
+ iconSprite: icon.type,
|
|
|
|
|
+ });
|
|
|
|
|
+ const next = new Set(placedDeviceIds.value);
|
|
|
|
|
+ next.add(icon.id);
|
|
|
|
|
+ placedDeviceIds.value = next;
|
|
|
|
|
+
|
|
|
|
|
+ const colors: colorInfo[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ colors.push({ id: icon.id, color: getMarkerColor(findDevice(icon.id)) });
|
|
|
|
|
+ viewerRef.value?.setIconsColor(colors);
|
|
|
|
|
+ let dev = { id: icon.id, other1: icon.coords[0], other2: icon.coords[1], other3: 1 };
|
|
|
|
|
+ onSubmit(dev);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //表单提交事件
|
|
|
|
|
+ async function onSubmit(deviceinfo) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await saveOrUpdate(deviceinfo, true);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ /** 拖动设备图标后位置更新回调 */
|
|
|
|
|
+ function onIconMoved(icon: any) {
|
|
|
|
|
+ console.log('Icon moved:', icon);
|
|
|
|
|
+ let dev = { id: icon.id, other1: null, other2: null, other3: 0 };
|
|
|
|
|
+ onSubmit(dev);
|
|
|
|
|
+ }
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除模式下图标被删除回调:接收被删图标的 id/type
|
|
|
|
|
+ * 若删除的正是当前弹框显示的设备,则关闭弹框并停止刷新定时器
|
|
|
|
|
+ */
|
|
|
|
|
+ function onIconDeleted(icon: { id: number; type: string }) {
|
|
|
|
|
+ console.log('Icon deleted:', icon);
|
|
|
|
|
+ if (currentDeviceIcon.value && currentDeviceIcon.value.id === icon.id) {
|
|
|
|
|
+ onDevicePopupClose();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 获取cad图纸数据
|
|
|
|
|
+ async function getCadData() {
|
|
|
|
|
+ // 如果是常村时
|
|
|
|
|
+ const token = getToken();
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ const db = window['CustomDB'];
|
|
|
|
|
+
|
|
|
|
|
+ const filedata = await db.modal.where('modalName').equals("cadfile").toArray();
|
|
|
|
|
+ if (filedata != null && filedata.length > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ loadCAD(filedata[0].modalVal)
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fetch(`${baseApiUrl}/sys/common/static/hsq/hsq.json`, {
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ cache: 'no-cache',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'application/force-download',
|
|
|
|
|
+ Authorization: token,
|
|
|
|
|
+ 'X-Access-Token': token,
|
|
|
|
|
+ token: token,
|
|
|
|
|
+ },
|
|
|
|
|
+ }).then(async (resp) => {
|
|
|
|
|
+ const data: CadData = await resp.json();
|
|
|
|
|
+ if(loading.value){
|
|
|
|
|
+ loadCAD(data)
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ var date = new Date()
|
|
|
|
|
+ var info = { modalName: "cadfile", modalVal: data ,versionStr: date.getHours()+"-"+date.getMinutes()+"-"+date.getSeconds()}
|
|
|
|
|
+ if (filedata != null && filedata.length > 0){
|
|
|
|
|
+ await db.modal.update('cadfile', info);
|
|
|
|
|
+ console.log("-----db.modal.update----cadfile----")
|
|
|
|
|
+ }
|
|
|
|
|
+ else{
|
|
|
|
|
+ await db.modal.add(info);
|
|
|
|
|
+ console.log("-----db.modal.update----cadfile----")
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ function loadCAD(data){
|
|
|
|
|
+ cadData.value = data;
|
|
|
|
|
+ console.log(`Loaded ${data.features.length} CAD features`);
|
|
|
|
|
+
|
|
|
|
|
+ if (START_IN_CAD_MODE && viewerRef.value) {
|
|
|
|
|
+ // 不飞向标注点,直接进入 CAD 全图视图(无预置图标)
|
|
|
|
|
+ viewerRef.value.switchToCadMode(undefined, data, []);
|
|
|
|
|
+ // 设置 CAD 画布背景色(深色背景,无默认图元颜色兜底)
|
|
|
|
|
+ viewerRef.value?.setCADMapColor(true, [255, 255, 255]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ /** 组件卸载前清理:兜底停止定时器,防止内存泄漏 */
|
|
|
|
|
+ onBeforeUnmount(() => {
|
|
|
|
|
+ if (devicePopupTimer) {
|
|
|
|
|
+ clearInterval(devicePopupTimer);
|
|
|
|
|
+ devicePopupTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ /** 页面挂载后加载 CAD 数据,并按配置决定初始视图 */
|
|
|
|
|
+ onMounted(async () => {
|
|
|
|
|
+ // 获取cad图纸数据
|
|
|
|
|
+ getCadData();
|
|
|
|
|
+ });
|
|
|
|
|
+ watch(
|
|
|
|
|
+ deviceList,
|
|
|
|
|
+ () => {
|
|
|
|
|
+ syncPlacedStateToDevices();
|
|
|
|
|
+ },
|
|
|
|
|
+ { deep: true }
|
|
|
|
|
+ );
|
|
|
|
|
+ onUnmounted(() => {});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="less" scoped>
|
|
|
|
|
+ .content {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .viewer-area,
|
|
|
|
|
+ .viewer-canvas {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .coord-bar {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 8px;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ padding: 6px 16px;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.78);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-family: Consolas, Monaco, monospace;
|
|
|
|
|
+ z-index: 200;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ > span {
|
|
|
|
|
+ margin: 0 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .placing-hint {
|
|
|
|
|
+ color: #faad14;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .device-panel {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 8px;
|
|
|
|
|
+ right: 8px;
|
|
|
|
|
+ width: 300px;
|
|
|
|
|
+ z-index: 300;
|
|
|
|
|
+ background: rgba(16, 28, 48, 0.45);
|
|
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.18);
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ backdrop-filter: blur(14px) saturate(1.35);
|
|
|
|
|
+ -webkit-backdrop-filter: blur(14px) saturate(1.35);
|
|
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);
|
|
|
|
|
+ &.collapsed {
|
|
|
|
|
+ width: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+ .panel-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.04);
|
|
|
|
|
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
+ .panel-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #e0e0e0;
|
|
|
|
|
+ }
|
|
|
|
|
+ .panel-toggle {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .panel-body {
|
|
|
|
|
+ padding: 6px;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.ant-table) {
|
|
|
|
|
+ background: transparent !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ :deep(.ant-table-thead > tr > th) {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.06) !important;
|
|
|
|
|
+ border-bottom-color: rgba(255, 255, 255, 0.1) !important;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
|
|
+ }
|
|
|
|
|
+ :deep(.ant-table-tbody > tr > td) {
|
|
|
|
|
+ background: transparent !important;
|
|
|
|
|
+ border-bottom-color: rgba(255, 255, 255, 0.06) !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ :deep(.ant-table-tbody > tr:hover > td) {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.08) !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .loading-mask {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset:0;
|
|
|
|
|
+ background:rgba(0,0,0,0.5);
|
|
|
|
|
+ display:flex;
|
|
|
|
|
+ justify-content:center;
|
|
|
|
|
+ align-items:center;
|
|
|
|
|
+ z-index:9999;
|
|
|
|
|
+}
|
|
|
|
|
+.loading-spinner{
|
|
|
|
|
+ width:40px;
|
|
|
|
|
+ height:40px;
|
|
|
|
|
+ border:4px solid rgba(255,255,255,0.3);
|
|
|
|
|
+ border-top:#fff solid 4px;
|
|
|
|
|
+ border-radius:50%;
|
|
|
|
|
+ animation:spin 1s linear infinite;
|
|
|
|
|
+}
|
|
|
|
|
+@keyframes spin{
|
|
|
|
|
+ to{transform:rotate(360deg)}
|
|
|
|
|
+}
|
|
|
|
|
+.loading-text{
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 54%;
|
|
|
|
|
+ z-index: 3;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|