Bläddra i källkod

CAD图纸组件更换,添加设备布点,布点信息保存,设备图标点击弹框显示功能

msx 1 månad sedan
förälder
incheckning
d87f08351e

+ 5 - 0
package.json

@@ -39,6 +39,11 @@
     "@vueuse/shared": "^10.4.1",
     "@x-viewer/core": "^0.21.23",
     "@zxcvbn-ts/core": "^3.0.3",
+    "maplibre-gl": "^4.7.1",
+    "@deck.gl/core": "^9.1.4",
+    "@deck.gl/layers": "^9.1.4",
+    "@deck.gl/mapbox": "^9.1.4",
+    "@deck.gl/extensions": "^9.1.4",
     "animate.css": "^4.1.1",
     "ant-design-vue": "^3.2.20",
     "axios": "^1.5.0",

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
src/components/map/index-BZx3Tuz5.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
src/components/map/map-cad-viewer.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 5 - 0
src/components/map/map-cad-viewer.es.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
src/components/map/map-cad-viewer.umd.js


+ 243 - 0
src/components/map/types/App.d.ts

@@ -0,0 +1,243 @@
+import { CadData, MapConfig, BoundaryConfig, MapMarker } from './types/cad';
+import { DefineComponent, ComponentOptionsMixin, PublicProps, ComponentProvideOptions, CreateComponentPublicInstanceWithMixins, Ref, ComputedRef, GlobalComponents, GlobalDirectives } from 'vue';
+import { CadFeature, ViewerMode } from '.';
+import { Map } from 'maplibre-gl';
+import { MapboxOverlay } from '@deck.gl/mapbox';
+import { LayerItem } from './components/LayerPanel';
+declare const _default: DefineComponent<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, ComponentProvideOptions, true, {
+    viewerRef: CreateComponentPublicInstanceWithMixins<Readonly<{
+        mapConfig?: Partial<MapConfig>;
+        boundaries?: BoundaryConfig;
+        markers?: MapMarker[];
+        cadJson?: CadData | string;
+        cadDataUrl?: string;
+        startInCadMode?: boolean;
+    }> & Readonly<{
+        "onMarker-click"?: ((marker: MapMarker) => any) | undefined;
+        "onMarker-open-cad"?: ((marker: MapMarker) => any) | undefined;
+        "onFeature-select"?: ((feature: CadFeature) => any) | undefined;
+        "onMode-change"?: ((mode: ViewerMode) => any) | undefined;
+        "onIcon-placed"?: ((icon: {
+            id: string;
+            type: string;
+            coords: [number, number];
+            name?: string;
+        }) => any) | undefined;
+        "onIcon-moved"?: ((icon: {
+            id: string;
+            type: string;
+            coords: [number, number];
+            name?: string;
+        }) => any) | undefined;
+        "onDevice-click"?: ((icon: {
+            id: string;
+            type: string;
+            name?: string;
+            coords: [number, number];
+        }) => any) | undefined;
+        "onDevice-popup-close"?: (() => any) | undefined;
+        "onIcon-deleted"?: ((icon: {
+            id: string;
+            type: string;
+        }) => any) | undefined;
+    }>, {
+        loadCadData: (data: CadData) => void;
+        switchToCadMode: (marker?: MapMarker, cadDataArg?: CadData, icons?: {
+            id: number;
+            type: string;
+            coords: number[];
+            name?: string;
+            iconUrl?: string;
+        }[]) => void;
+        loadIcons: (icons?: {
+            id: number;
+            type: string;
+            coords: number[];
+            name?: string;
+            iconUrl?: string;
+        }[]) => void;
+        goBackToMap: () => void;
+        getMapInstance: () => Map | null;
+        getDeckOverlay: () => MapboxOverlay | null;
+        setMode: (m: ViewerMode) => void;
+        toggleDrawingMode: (type: string, id?: string) => void;
+        toggleDeleteMode: () => void;
+        isDeleteMode: () => boolean;
+        zoomToFit: () => void;
+        getCityCenter: (name: string) => Promise<[number, number] | null>;
+        clearPlacedIcons: () => void;
+        setIconsColor: (colors: {
+            id: number;
+            color: number[];
+        }[]) => void;
+        mode: Ref<ViewerMode, ViewerMode>;
+        isDrawingMode: () => boolean;
+        placedIconsCount: () => number;
+        showDevicePopup: (id: number | string, type: string, html: string) => void;
+        svgIcons: Ref<{
+            id: string;
+            label: string;
+            svg: string;
+        }[], {
+            id: string;
+            label: string;
+            svg: string;
+        }[] | {
+            id: string;
+            label: string;
+            svg: string;
+        }[]>;
+        selectedIconId: Ref<string, string>;
+        setSelectedIcon: (id: string) => void;
+        setDrawingTool: (type: string, id?: string) => void;
+        deletePlacedIcon: (id: string) => boolean;
+        layerPanelVisible: Ref<boolean, boolean>;
+        toggleLayerPanel: () => void;
+        layerItems: ComputedRef< LayerItem[]>;
+        modeValue: ViewerMode;
+        changeView: (newBoundaries?: BoundaryConfig, newMarkers?: MapMarker[], level?: 1 | 2 | 3) => void;
+        setMapView: (center: [number, number], zoom: number, duration?: number) => void;
+        setCADMapColor: (isDark: boolean, color: [number, number, number]) => void;
+    }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
+        "marker-click": (marker: MapMarker) => any;
+        "marker-open-cad": (marker: MapMarker) => any;
+        "feature-select": (feature: CadFeature) => any;
+        "mode-change": (mode: ViewerMode) => any;
+        "icon-placed": (icon: {
+            id: string;
+            type: string;
+            coords: [number, number];
+            name?: string;
+        }) => any;
+        "icon-moved": (icon: {
+            id: string;
+            type: string;
+            coords: [number, number];
+            name?: string;
+        }) => any;
+        "device-click": (icon: {
+            id: string;
+            type: string;
+            name?: string;
+            coords: [number, number];
+        }) => any;
+        "device-popup-close": () => any;
+        "icon-deleted": (icon: {
+            id: string;
+            type: string;
+        }) => any;
+    }, PublicProps, {
+        mapConfig: Partial<MapConfig>;
+        boundaries: BoundaryConfig;
+        markers: MapMarker[];
+    }, false, {}, {}, GlobalComponents, GlobalDirectives, string, {
+        containerRef: HTMLDivElement;
+        mapContainerRef: HTMLDivElement;
+        cadCanvasRef: HTMLDivElement;
+    }, HTMLDivElement, ComponentProvideOptions, {
+        P: {};
+        B: {};
+        D: {};
+        C: {};
+        M: {};
+        Defaults: {};
+    }, Readonly<{
+        mapConfig?: Partial<MapConfig>;
+        boundaries?: BoundaryConfig;
+        markers?: MapMarker[];
+        cadJson?: CadData | string;
+        cadDataUrl?: string;
+        startInCadMode?: boolean;
+    }> & Readonly<{
+        "onMarker-click"?: ((marker: MapMarker) => any) | undefined;
+        "onMarker-open-cad"?: ((marker: MapMarker) => any) | undefined;
+        "onFeature-select"?: ((feature: CadFeature) => any) | undefined;
+        "onMode-change"?: ((mode: ViewerMode) => any) | undefined;
+        "onIcon-placed"?: ((icon: {
+            id: string;
+            type: string;
+            coords: [number, number];
+            name?: string;
+        }) => any) | undefined;
+        "onIcon-moved"?: ((icon: {
+            id: string;
+            type: string;
+            coords: [number, number];
+            name?: string;
+        }) => any) | undefined;
+        "onDevice-click"?: ((icon: {
+            id: string;
+            type: string;
+            name?: string;
+            coords: [number, number];
+        }) => any) | undefined;
+        "onDevice-popup-close"?: (() => any) | undefined;
+        "onIcon-deleted"?: ((icon: {
+            id: string;
+            type: string;
+        }) => any) | undefined;
+    }>, {
+        loadCadData: (data: CadData) => void;
+        switchToCadMode: (marker?: MapMarker, cadDataArg?: CadData, icons?: {
+            id: number;
+            type: string;
+            coords: number[];
+            name?: string;
+            iconUrl?: string;
+        }[]) => void;
+        loadIcons: (icons?: {
+            id: number;
+            type: string;
+            coords: number[];
+            name?: string;
+            iconUrl?: string;
+        }[]) => void;
+        goBackToMap: () => void;
+        getMapInstance: () => Map | null;
+        getDeckOverlay: () => MapboxOverlay | null;
+        setMode: (m: ViewerMode) => void;
+        toggleDrawingMode: (type: string, id?: string) => void;
+        toggleDeleteMode: () => void;
+        isDeleteMode: () => boolean;
+        zoomToFit: () => void;
+        getCityCenter: (name: string) => Promise<[number, number] | null>;
+        clearPlacedIcons: () => void;
+        setIconsColor: (colors: {
+            id: number;
+            color: number[];
+        }[]) => void;
+        mode: Ref<ViewerMode, ViewerMode>;
+        isDrawingMode: () => boolean;
+        placedIconsCount: () => number;
+        showDevicePopup: (id: number | string, type: string, html: string) => void;
+        svgIcons: Ref<{
+            id: string;
+            label: string;
+            svg: string;
+        }[], {
+            id: string;
+            label: string;
+            svg: string;
+        }[] | {
+            id: string;
+            label: string;
+            svg: string;
+        }[]>;
+        selectedIconId: Ref<string, string>;
+        setSelectedIcon: (id: string) => void;
+        setDrawingTool: (type: string, id?: string) => void;
+        deletePlacedIcon: (id: string) => boolean;
+        layerPanelVisible: Ref<boolean, boolean>;
+        toggleLayerPanel: () => void;
+        layerItems: ComputedRef< LayerItem[]>;
+        modeValue: ViewerMode;
+        changeView: (newBoundaries?: BoundaryConfig, newMarkers?: MapMarker[], level?: 1 | 2 | 3) => void;
+        setMapView: (center: [number, number], zoom: number, duration?: number) => void;
+        setCADMapColor: (isDark: boolean, color: [number, number, number]) => void;
+    }, {}, {}, {}, {
+        mapConfig: Partial<MapConfig>;
+        boundaries: BoundaryConfig;
+        markers: MapMarker[];
+    }> | null;
+}, HTMLDivElement>;
+export default _default;

+ 21 - 0
src/components/map/types/assets/boundaries/index.d.ts

@@ -0,0 +1,21 @@
+/**
+ * 边界数据加载器
+ *
+ * 使用 Vite 的 import.meta.glob 在构建时将边界 JSON 打包进产物,
+ * 运行时直接从本地加载,无需 API 请求。
+ *
+ * 如果某个 adcode 的边界文件不存在,会静默返回 null。
+ */
+/**
+ * 根据 adcode 加载边界 GeoJSON
+ * @param adcode 行政区划代码
+ * @param mode 加载模式:
+ *   - 'auto'(默认):优先加载 _full 文件(含下级边界),不存在时回退基础文件
+ *   - 'base':仅加载基础文件({adcode}.json,只含本级边界,如市级基础文件含市本级)
+ *   - 'full':仅加载 _full 文件({adcode}_full.json,含本级+下级,如市级 _full 含区县)
+ */
+export declare function loadBoundaryByAdcode(adcode: number, mode?: 'auto' | 'base' | 'full'): Promise<any | null>;
+/**
+ * 批量加载多个 adcode 的边界数据
+ */
+export declare function loadBoundaries(adcodes: number[]): Promise<Map<number, any>>;

+ 18 - 0
src/components/map/types/components/InfoPopup.d.ts

@@ -0,0 +1,18 @@
+import { MarkerPopup } from '../types/cad';
+import { DefineComponent, ComponentOptionsMixin, PublicProps, ComponentProvideOptions } from 'vue';
+type __VLS_Props = {
+    visible: boolean;
+    x: number;
+    y: number;
+    title: string;
+    info?: Record<string, string | number>;
+    sections?: MarkerPopup['items'];
+    /** 自定义 HTML 内容(APP.vue 动态拼接,优先于 info/sections 渲染) */
+    html?: string;
+};
+declare const _default: DefineComponent<__VLS_Props, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
+    close: () => any;
+}, string, PublicProps, Readonly<__VLS_Props> & Readonly<{
+    onClose?: (() => any) | undefined;
+}>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
+export default _default;

+ 22 - 0
src/components/map/types/components/LayerPanel.d.ts

@@ -0,0 +1,22 @@
+import { DefineComponent, ComponentOptionsMixin, PublicProps, ComponentProvideOptions } from 'vue';
+export interface LayerItem {
+    id: number;
+    visible: boolean;
+    name?: string;
+    count: number;
+    color: string;
+}
+type __VLS_Props = {
+    visible: boolean;
+    layers: LayerItem[];
+};
+declare const _default: DefineComponent<__VLS_Props, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
+    close: () => any;
+    "toggle-layer": (id: number) => any;
+    "toggle-all": (visible: boolean) => any;
+}, string, PublicProps, Readonly<__VLS_Props> & Readonly<{
+    onClose?: (() => any) | undefined;
+    "onToggle-layer"?: ((id: number) => any) | undefined;
+    "onToggle-all"?: ((visible: boolean) => any) | undefined;
+}>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
+export default _default;

+ 241 - 0
src/components/map/types/components/MapCadViewer.d.ts

@@ -0,0 +1,241 @@
+import { default as maplibregl } from 'maplibre-gl';
+import { MapboxOverlay } from '@deck.gl/mapbox';
+import { LayerItem } from './LayerPanel';
+import { CadFeature, CadData, ViewerMode, MapMarker, BoundaryConfig, MapConfig } from '../types/cad';
+import { DefineComponent, Ref, ComputedRef, ComponentOptionsMixin, PublicProps, ComponentProvideOptions } from 'vue';
+type __VLS_Props = {
+    mapConfig?: Partial<MapConfig>;
+    boundaries?: BoundaryConfig;
+    markers?: MapMarker[];
+    cadJson?: CadData | string;
+    cadDataUrl?: string;
+    /** 启动时默认进入 CAD 模式(不显示地图) */
+    startInCadMode?: boolean;
+};
+/**
+ * 根据城市/区县名称获取其中心点坐标 [经度, 纬度]
+ * 从本地边界 GeoJSON 的 properties.center / centroid 字段直接读取(数据源自带中心点),
+ * 无需额外 API 请求。
+ * @param name 城市或区县名称,如 "太原市"、"海淀区"、"山西省"
+ * @returns Promise<[lng, lat] | null> 未找到匹配区域时返回 null
+ */
+declare function getCityCenter(name: string): Promise<[number, number] | null>;
+/**
+ * 切换到 CAD 图纸视图
+ * @param marker 可选:从地图标注点进入时传入,用于 flyTo 定位
+ * @param cadDataArg 可选:CAD 原始数据(未传且无缓存时直接返回)
+ * @param icons 可选:之前在地图/绘制过程中产生的图标数组,
+ *              每个元素为 { id, type, coords, name?, iconUrl? },
+ *              coords 为 CAD 投影坐标(与图纸坐标系一致),
+ *              切换进入 CAD 视图后这些图标会按类型和坐标一并绘制出来。
+ */
+declare function switchToCadMode(marker?: MapMarker, cadDataArg?: CadData, icons?: {
+    id: number;
+    type: string;
+    coords: number[];
+    name?: string;
+    iconUrl?: string;
+}[]): void;
+declare function loadIcons(icons?: {
+    id: number;
+    type: string;
+    coords: number[];
+    name?: string;
+    iconUrl?: string;
+}[]): void;
+/**
+ * 返回地图视图:恢复地图显示与进入前的中心/缩放,
+ * 清理解除拖拽监听,保留 CAD Deck 实例(GPU 缓存)以便下次快速进入。
+ */
+declare function goBackToMap(): void;
+/**
+ * 由 APP.vue 调用:把动态拼接好的弹框 HTML 传入并显示设备弹框。
+ * 定时器更新数据时重复调用此方法即可刷新内容(位置保持不变)。
+ * @param id 设备 id
+ * @param type 设备类型
+ * @param html 动态拼接的弹框 HTML 内容
+ */
+declare function showDevicePopup(id: number | string, type: string, html: string): void;
+/**
+ * 外部可调用此方法加载 CAD 数据(暴露给父组件)
+ * @param data CAD 原始数据
+ */
+declare function loadCadDataExternal(data: CadData): void;
+/**
+ * 切换绘制模式:进入后左键点击图纸放置设备图标,中键拖动平移
+ * @param type 当前绘制的设备图标类型(gate/modelsensor/obfurage/speed)
+ */
+declare function toggleDrawingMode(type: string, id?: string): void;
+/** 删除模式切换:点击设备图标删除该图标,其余点击忽略 */
+declare function toggleDeleteMode(): void;
+/**
+ * 切换绘制工具类型(不退出绘制模式)
+ * @param type 设备图标类型
+ */
+declare function setDrawingTool(type: string, id?: string): void;
+/**
+ * 删除指定 id 的已放置图标(App.vue 可编程调用)
+ * @param id 设备图标 id
+ * @returns 是否删除成功
+ */
+declare function deletePlacedIcon(id: string): boolean;
+/** 清除图纸上所有已绘制的设备图标 */
+declare function clearPlacedIcons(): void;
+/**
+ * 为指定 id 的设备图标设置颜色(染色)
+ * @param colors 颜色配置数组 [{ id, color }]
+ *               id:图标 id(数字,内部自动与 string id 匹配)
+ *               color:RGB 颜色 [r, g, b](0-255)
+ * 仅更新传入的对应 id 图标,其余图标颜色保持不变
+ */
+declare function setIconsColor(colors: {
+    id: number;
+    color: number[];
+}[]): void;
+/** 缩放至全图:重置视图为 CAD 图纸整体范围 */
+declare function zoomToFit(): void;
+declare const _default: DefineComponent<__VLS_Props, {
+    loadCadData: typeof loadCadDataExternal;
+    switchToCadMode: typeof switchToCadMode;
+    loadIcons: typeof loadIcons;
+    goBackToMap: typeof goBackToMap;
+    getMapInstance: () => maplibregl.Map | null;
+    getDeckOverlay: () => MapboxOverlay | null;
+    setMode: (m: ViewerMode) => void;
+    /** CAD 模式 / 绘制模式切换 */
+    toggleDrawingMode: typeof toggleDrawingMode;
+    /** 删除模式切换(点击设备图标删除) */
+    toggleDeleteMode: typeof toggleDeleteMode;
+    /** 是否为删除模式 */
+    isDeleteMode: () => boolean;
+    /** 缩放至全图 */
+    zoomToFit: typeof zoomToFit;
+    /** 根据城市/区县名称获取中心点坐标(经纬度) */
+    getCityCenter: typeof getCityCenter;
+    /** 清除绘制的图标 */
+    clearPlacedIcons: typeof clearPlacedIcons;
+    /** 为指定 id 的设备图标设置颜色 [{id, color:[r,g,b]}] */
+    setIconsColor: typeof setIconsColor;
+    /** 当前模式 */
+    mode: Ref<ViewerMode, ViewerMode>;
+    /** 是否为绘制模式 */
+    isDrawingMode: () => boolean;
+    /** 已放置的图标数 */
+    placedIconsCount: () => number;
+    /** 由 APP.vue 调用:传入动态拼接的 HTML 更新设备弹框内容 */
+    showDevicePopup: typeof showDevicePopup;
+    /** SVG 图标列表 */
+    svgIcons: Ref<{
+        id: string;
+        label: string;
+        svg: string;
+    }[], {
+        id: string;
+        label: string;
+        svg: string;
+    }[] | {
+        id: string;
+        label: string;
+        svg: string;
+    }[]>;
+    /** 当前选中的图标 ID */
+    selectedIconId: Ref<string, string>;
+    /** 设置图标 ID */
+    setSelectedIcon: (id: string) => void;
+    /** 设置绘制工具 (不退出绘制模式) */
+    setDrawingTool: typeof setDrawingTool;
+    deletePlacedIcon: typeof deletePlacedIcon;
+    /** 图层面板可见性 */
+    layerPanelVisible: Ref<boolean, boolean>;
+    /** 切换图层面板 */
+    toggleLayerPanel: () => void;
+    /** 图层列表 */
+    layerItems: ComputedRef<LayerItem[]>;
+    modeValue: ViewerMode;
+    /**
+     * 更新地图视图(边界蒙版 + 标注点)
+     * 在 map 模式下调用,替换当前 boundaryConfig 和 markers 并重新绘制
+     * @param newBoundaries 新的行政区划边界配置
+     * @param newMarkers 新的标注点集合
+     * @param level 边界区域等级:1=省级、2=市级、3=县级(决定悬停弹框显示哪级名称)
+     */
+    changeView: (newBoundaries?: BoundaryConfig, newMarkers?: MapMarker[], level?: 1 | 2 | 3) => void;
+    /**
+     * 设置地图视图(中心点 + 缩放级别),仅在地图模式下生效
+     * @param center [经度, 纬度]
+     * @param zoom 缩放级别
+     * @param duration 飞行动画时长(毫秒),默认 500
+     */
+    setMapView: (center: [number, number], zoom: number, duration?: number) => void;
+    /**
+     * 设置地图底色和默认文字颜色
+     * @param isDark 是否深色
+     * @param color 默认文字颜色
+     */
+    setCADMapColor: (isDark: boolean, color: [number, number, number]) => void;
+}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {
+    "marker-click": (marker: MapMarker) => any;
+    "marker-open-cad": (marker: MapMarker) => any;
+    "feature-select": (feature: CadFeature) => any;
+    "mode-change": (mode: ViewerMode) => any;
+    "icon-placed": (icon: {
+        id: string;
+        type: string;
+        coords: [number, number];
+        name?: string;
+    }) => any;
+    "icon-moved": (icon: {
+        id: string;
+        type: string;
+        coords: [number, number];
+        name?: string;
+    }) => any;
+    "device-click": (icon: {
+        id: string;
+        type: string;
+        name?: string;
+        coords: [number, number];
+    }) => any;
+    "device-popup-close": () => any;
+    "icon-deleted": (icon: {
+        id: string;
+        type: string;
+    }) => any;
+}, string, PublicProps, Readonly<__VLS_Props> & Readonly<{
+    "onMarker-click"?: ((marker: MapMarker) => any) | undefined;
+    "onMarker-open-cad"?: ((marker: MapMarker) => any) | undefined;
+    "onFeature-select"?: ((feature: CadFeature) => any) | undefined;
+    "onMode-change"?: ((mode: ViewerMode) => any) | undefined;
+    "onIcon-placed"?: ((icon: {
+        id: string;
+        type: string;
+        coords: [number, number];
+        name?: string;
+    }) => any) | undefined;
+    "onIcon-moved"?: ((icon: {
+        id: string;
+        type: string;
+        coords: [number, number];
+        name?: string;
+    }) => any) | undefined;
+    "onDevice-click"?: ((icon: {
+        id: string;
+        type: string;
+        name?: string;
+        coords: [number, number];
+    }) => any) | undefined;
+    "onDevice-popup-close"?: (() => any) | undefined;
+    "onIcon-deleted"?: ((icon: {
+        id: string;
+        type: string;
+    }) => any) | undefined;
+}>, {
+    mapConfig: Partial<MapConfig>;
+    boundaries: BoundaryConfig;
+    markers: MapMarker[];
+}, {}, {}, {}, string, ComponentProvideOptions, false, {
+    containerRef: HTMLDivElement;
+    mapContainerRef: HTMLDivElement;
+    cadCanvasRef: HTMLDivElement;
+}, HTMLDivElement>;
+export default _default;

+ 28 - 0
src/components/map/types/index.d.ts

@@ -0,0 +1,28 @@
+/**
+ * Map-CAD Viewer
+ *
+ * Vue3 组件 — 在 MapLibre GL JS 地图上叠加 CAD 图纸数据。
+ *
+ * ## 使用
+ *
+ * ```vue
+ * <template>
+ *   <MapCadViewer
+ *     :map-config="{ center: [116.4, 39.9], zoom: 12 }"
+ *     :cad-data-url="'/features.json'"
+ *     :markers="[{ coords: [116.4, 39.9], label: '1' }]"
+ *   />
+ * </template>
+ *
+ * <script setup>
+ * import MapCadViewer from 'map-cad-viewer'
+ * </script>
+ * ```
+ */
+export { default as MapCadViewer } from './components/MapCadViewer';
+export { default as InfoPopup } from './components/InfoPopup';
+export { default as LayerPanel } from './components/LayerPanel';
+export type { CadFeature, CadColor, CadEntityType, CadData, ViewerMode, MapMarker, MarkerPopup, BoundaryConfig, MapConfig, CoordTransform, LayerState, Point2D, Point3D, LineProperties, LWPolylineProperties, HatchProperties, TextProperties, MTextProperties, } from './types/cad';
+export { parseCadFeatures, getFeatureInfo, getEntityTypeLabel, } from './utils/cadParser';
+export { cadColorToRgba, aciToRgb, isColorByLayer, isColorByBlock, parseCadRgb, resolveFeatureColor, } from './utils/colorUtils';
+export { cadToMap, createCadToMapFn, analyzeCadCoords, computeBounds, DEFAULT_SCALE, } from './utils/coordTransform';

+ 1 - 0
src/components/map/types/main.d.ts

@@ -0,0 +1 @@
+export {};

+ 289 - 0
src/components/map/types/types/cad.d.ts

@@ -0,0 +1,289 @@
+/**
+ * CAD 数据与地图应用 TypeScript 类型定义
+ *
+ * 本文件定义了整个项目中使用的所有数据结构和接口,
+ * 涵盖 CAD JSON 解析、地图配置、标注点、边界等场景。
+ *
+ * ## 数据流
+ * features.json → CadData → expandBlockInserts() → CadFeature[]
+ *               → parseCadFeatures() → ParsedCadData → deck.gl 图层
+ */
+/** CAD 颜色信息(来自 librecad 解析) */
+export interface CadColor {
+    flag: number;
+    method: number;
+    alpha_raw: number;
+    alpha: number;
+    /** CAD 颜色索引 (1=红, 2=黄, 3=绿, 4=青, 5=蓝, 6=品红, 7=白/黑) */
+    index: number;
+    raw: number;
+    /** 32-bit RGBA 打包整数 */
+    rgb: number;
+    alpha_type: number;
+}
+/** 二维坐标点 */
+export interface Point2D {
+    x: number;
+    y: number;
+}
+/** 三维坐标点 */
+export interface Point3D {
+    x: number;
+    y: number;
+    z: number;
+}
+/** 拉伸方向向量 */
+export interface Extrusion {
+    x: number;
+    y: number;
+    z: number;
+}
+/** 线段 (LINE) */
+export interface LineProperties {
+    z_is_zero: number;
+    extrusion: Extrusion;
+    start: Point3D;
+    end: Point3D;
+}
+/** 多段线路径中的一个点(含凸度 bulge) */
+export interface PolylinePathPoint {
+    /** 凸度值:0=直线, 正数=顺时针弧, 负数=逆时针弧 */
+    bulge: number;
+    point: Point2D;
+}
+/** 填充路径中的一段 */
+export interface HatchPath {
+    num_segs_or_paths: number;
+    flag: number;
+    polyline_paths: PolylinePathPoint[];
+    bulges_present: number;
+    closed: number;
+}
+/** 填充 (HATCH) */
+export interface HatchProperties {
+    is_solid_fill: number;
+    pixel_size: number;
+    is_gradient_fill: number;
+    gradient_tint: number;
+    colors: any[];
+    gradient_angle: number;
+    num_seeds: number;
+    double_flag: number;
+    extrusion: Extrusion;
+    angle: number;
+    gradient_name: string;
+    scale_spacing: number;
+    single_color_gradient: number;
+    elevation: number;
+    pattern_type: number;
+    deflines: any[];
+    seeds: Point2D[];
+    num_colors: number;
+    num_deflines: number;
+    has_derived: number;
+    is_associative: number;
+    reserved: number;
+    paths: HatchPath[];
+    /** 填充图案名称,如 "SOLID" 表示实心填充 */
+    name: string;
+    num_paths: number;
+    style: number;
+    gradient_shift: number;
+}
+/** 轻量多段线 (LWPOLYLINE) */
+export interface LWPolylineProperties {
+    elevation: number;
+    bulges: number[];
+    flag: number;
+    vertexids: number[];
+    num_bulges: number;
+    extrusion: Extrusion;
+    num_widths: number;
+    widths: any[];
+    num_vertexids: number;
+    num_points: number;
+    const_width: number;
+    points: Point2D[];
+}
+/** 单行文字 (TEXT) */
+export interface TextProperties {
+    elevation: number;
+    generation: number;
+    horiz_alignment: number;
+    oblique_angle: number;
+    /** 插入点坐标 */
+    ins_pt: Point2D;
+    /** 旋转角度(弧度) */
+    rotation: number;
+    dataflags: number;
+    text_value: string;
+    extrusion: Extrusion;
+    /** 对齐参考点 */
+    alignment_pt: Point2D;
+    width_factor: number;
+    vert_alignment: number;
+    /** 文字高度 */
+    height: number;
+}
+/** 多行文字 (MTEXT) */
+export interface MTextProperties {
+    flow_dir: number;
+    numfragments: number;
+    ins_pt: Point3D;
+    is_not_annotative: number;
+    linespace_style: number;
+    unknown_b0: number;
+    bg_fill_trans: number;
+    bg_fill_flag: number;
+    /** 多行文字附着点:1=左上, 2=中上, 3=右上, 4=左中, 5=正中, 6=右中, 7=左下, 8=中下, 9=右下 */
+    attachment: number;
+    class_version: number;
+    extrusion: Extrusion;
+    extents_width: number;
+    extents_height: number;
+    bg_fill_color: CadColor;
+    column_width: number;
+    rect_height: number;
+    text: string;
+    linespace_factor: number;
+    bg_fill_scale: number;
+    default_flag: number;
+    rect_width: number;
+    column_heights: number[];
+    ignore_attachment: number;
+    text_height: number;
+    flow_reversed: number;
+    x_axis_dir?: Point3D;
+}
+/** 所有支持的 CAD 图元类型 */
+export type CadEntityType = 'LINE' | 'LWPOLYLINE' | 'HATCH' | 'TEXT' | 'MTEXT';
+/** 单个 CAD 图元(解析后展开的扁平结构) */
+export interface CadFeature {
+    supertype: number;
+    color: CadColor;
+    id: number;
+    typename: CadEntityType;
+    /** 所在图层编号 */
+    layer: number;
+    /** 属主句柄(用于块引用时指向 objectMap 中的块定义) */
+    ownerhandle?: string | number;
+    blockFlag?: number;
+    blockName?: string;
+    properties: LineProperties | LWPolylineProperties | HatchProperties | TextProperties | MTextProperties;
+}
+/** 块插入参数 — objectMap 中每个 key 对应一个 INSERT 图元 */
+export interface BlockInsertDef {
+    /** 插入点坐标 */
+    ins_pt: Point3D;
+    /** 旋转角度(弧度) */
+    rotation: number;
+    /** 三轴缩放 */
+    scale: {
+        x: number;
+        y: number;
+        z: number;
+    };
+}
+/** 图层定义 */
+export interface CadLayer {
+    id: number;
+    name: string;
+    flag0: number;
+    frozen: number;
+    off: number;
+    frozen_in_new: number;
+    locked: number;
+    plotflag: number;
+    linewt: number;
+    color: CadColor;
+}
+/** 顶层 JSON 结构(features.json 格式) */
+export interface CadData {
+    /** 所有 CAD 图元 */
+    features: CadFeature[];
+    /** 图层定义列表 */
+    layers?: CadLayer[];
+    /** 块定义映射表:key=句柄(handle), value=块定义内的图元数组 */
+    objectMap?: Record<string, {
+        properties: BlockInsertDef;
+    }>;
+}
+/** 视图模式 */
+export type ViewerMode = 'map' | 'cad' | 'drawing' | 'deleting';
+/** 标注点弹窗内容结构 */
+export interface MarkerPopup {
+    title?: string;
+    html?: string;
+    items: {
+        subtitle: string;
+        content: string;
+    }[];
+}
+/** 地图标注点 */
+export interface MapMarker {
+    id: number;
+    /** 坐标 [经度, 纬度]。提供时按坐标显示;缺失时用 regionName 的中心点定位 */
+    coords?: [number, number];
+    /** 省/市/县名称(如 "太原市"、"小店区")。无 coords 时自动取其中心点作为标识位置 */
+    regionName?: string;
+    /** 标注点显示的数字 */
+    label: string;
+    /** 圆点填充色 RGBA,默认 [255, 200, 50, 200] */
+    color?: RGBA;
+    /** 数字字体色 RGBA,默认 [0, 0, 0, 255] */
+    fontColor?: RGBA;
+    /** 圆点边框色 RGBA,默认 [255, 255, 255, 220] */
+    borderColor?: RGBA;
+    /** 点击标题时是否切换到 CAD 模式(需配合 cadData 使用) */
+    switchToCad?: boolean;
+    /** 自定义弹窗内容(鼠标悬停时显示) */
+    popup?: MarkerPopup;
+    /** 关联的 CAD 数据 */
+    cadData?: string | CadData;
+    cadDataUrl?: string;
+}
+/** RGBA 颜色四元组,0-255 */
+export type RGBA = [number, number, number, number];
+/** 单个区域的配色方案 */
+export interface RegionColor {
+    fillColor?: RGBA;
+    strokeColor?: RGBA;
+}
+/** 省市区边界配置 */
+export interface BoundaryConfig {
+    provinces?: string[];
+    cities?: string[];
+    counties?: string[];
+    /** 默认填充色 RGBA */
+    fillColor?: RGBA;
+    /** 默认边线色 RGBA */
+    strokeColor?: RGBA;
+    /** 按区域名称单独设置颜色,key=区域名称(如 "河北省") */
+    regionColors?: Record<string, RegionColor>;
+}
+/** 底图配置 */
+export interface MapConfig {
+    /** 初始中心点 [经度, 纬度] */
+    center: [number, number];
+    /** 初始缩放级别 */
+    zoom: number;
+    /** 瓦片地图 URL 模板,支持 {z}/{x}/{y} 占位符 */
+    tileUrl?: string;
+    maxZoom?: number;
+}
+/** CAD 坐标 → 经纬度变换参数 */
+export interface CoordTransform {
+    /** 方式1: proj4 投影字符串 */
+    projection?: string;
+    /** 方式2: 简单偏移量 */
+    offsetX?: number;
+    offsetY?: number;
+    /** 方式2: 缩放系数 */
+    scale?: number;
+}
+/** 图层开关状态 */
+export interface LayerState {
+    id: number;
+    visible: boolean;
+    name?: string;
+}

+ 12 - 0
src/components/map/types/utils/cadParseWorker.d.ts

@@ -0,0 +1,12 @@
+/**
+ * CAD 数据解析 Web Worker
+ *
+ * 在 Worker 线程中执行 expandBlockInserts + parseCadFeatures + 边界计算,
+ * 避免几百 MB CAD 数据在主线程上阻塞 UI。
+ *
+ * 接收: { type: 'parse', data: CadData }
+ * 发送: { type: 'result', parsedCad, bounds, denseCenter, denseSpan, layerIds }
+ *        { type: 'progress', phase: string }
+ *        { type: 'error', message: string }
+ */
+export {};

+ 107 - 0
src/components/map/types/utils/cadParser.d.ts

@@ -0,0 +1,107 @@
+import { CadFeature, CadEntityType, Point2D, CadData, CadColor } from '../types/cad';
+/** 生成 HATCH deflines 图案的平行线线段 */
+export declare function generateHatchLines(boundaryRings: [number, number][][], deflines: {
+    angle: number;
+    base_point?: Point2D;
+    offset?: Point2D;
+}[], scaleSpacing: number, globalAngle: number): [number, number][][];
+/** 解析后的线段数据 */
+export interface ParsedPath {
+    id: string;
+    path: [number, number][];
+    color: [number, number, number, number];
+    layer: number;
+    sourceFeature: CadFeature;
+    /** 线宽(CAD 绘图单位),0 = 默认细线(随图层/打印样式) */
+    lineWidth?: number;
+}
+/** 解析后的多边形(填充)数据 */
+export interface ParsedPolygon {
+    id: string;
+    polygon: [number, number][][];
+    color: [number, number, number, number];
+    layer: number;
+    sourceFeature: CadFeature;
+}
+/** 解析后的填充图案线段(deflines 生成) */
+export interface ParsedHatchLine {
+    id: string;
+    path: [number, number][];
+    color: [number, number, number, number];
+    layer: number;
+    sourceFeature: CadFeature;
+}
+/** 解析后的文本标注数据 */
+export interface ParsedText {
+    id: string;
+    position: [number, number];
+    text: string;
+    color: [number, number, number, number];
+    size: number;
+    angle: number;
+    layer: number;
+    sourceFeature: CadFeature;
+    textAnchor?: string;
+    alignmentBaseline?: string;
+    /** MTEXT 背景填充标志位:0=无, 1=纯色填充, 2=画布背景色, 16=外边框, 可组合 */
+    bgFillFlag?: number;
+    /** MTEXT 背景填充色(bgFillFlag 含 bit0 时生效) */
+    bgFillColor?: [number, number, number, number];
+    /** MTEXT 背景边框色 */
+    bgBorderColor?: [number, number, number, number];
+    /** MTEXT 文字四周留白边距缩放因子 */
+    bgFillScale?: number;
+}
+/** 全部解析结果 */
+export interface ParsedCadData {
+    paths: ParsedPath[];
+    polygons: ParsedPolygon[];
+    hatchLines: ParsedHatchLine[];
+    texts: ParsedText[];
+    layerIds: number[];
+    cadCenterX: number;
+    cadCenterY: number;
+}
+/**
+ * 解析 CAD 特征数组为 deck.gl 图层数据
+ *
+ * 遍历所有 CadFeature,按 typename 分类处理:
+ * - LINE → ParsedPath
+ * - LWPOLYLINE → 闭合且 ≥3 顶点→ParsedPolygon,否则→ParsedPath
+ * - HATCH → 纯色→ParsedPolygon,deflines→generateHatchLines→ParsedHatchLine,回退→ParsedPolygon
+ * - TEXT → ParsedText
+ * - MTEXT → parseMText 清理格式化码后→ParsedText
+ *
+ * 颜色解析遵循 AutoCAD 颜色优先级:
+ * - TrueColor (method=0xC3) > ACI (index 1~255) > BYBLOCK > BYLAYER
+ * - BYLAYER 的颜色从 layerColorMap 中获取
+ *
+ * @param features CAD 特征列表
+ * @param mapCenter [lng, lat] 坐标变换目标中心
+ * @param scale 缩放系数(默认 1e-5)
+ * @param rawMode true=原始 CAD 坐标,false=变换到经纬度
+ * @param layerColorMap 图层ID→颜色 的映射(用于 BYLAYER 解析)
+ */
+export declare function parseCadFeatures(features: CadFeature[], mapCenter?: [number, number], scale?: number, rawMode?: boolean, layerColorMap?: Record<number, CadColor>, defaultColor?: number[]): ParsedCadData;
+/** 从 URL 加载 features.json 数据 */
+export declare function loadCadData(url: string): Promise<CadData>;
+/** 获取图元类型的中文名称 */
+export declare function getEntityTypeLabel(typename: CadEntityType): string;
+/** 获取特征的显示信息(用于点击弹出面板) */
+export declare function getFeatureInfo(feat: CadFeature): Record<string, string | number>;
+/**
+ * 展开块引用(INSERT)
+ *
+ * 遍历所有 features,对带有 ownerhandle 的图元,查找 objectMap 中的
+ * 块定义,将块内图元的坐标经过缩放→旋转→平移变换后克隆为独立图元。
+ *
+ * 变换公式:
+ *   x' = px*sx*cos(rot) - py*sy*sin(rot) + insPt.x
+ *   y' = px*sx*sin(rot) + py*sy*cos(rot) + insPt.y
+ *
+ * 跳过名称包含 "lr_" 的块(布局/视口相关块)。
+ *
+ * @param data 原始 CAD JSON 数据
+ * @returns 展开后的扁平 CadFeature[],块引用中的坐标已完成变换
+ */
+export declare function expandBlockInserts(data: CadData): CadFeature[];

+ 65 - 0
src/components/map/types/utils/colorUtils.d.ts

@@ -0,0 +1,65 @@
+import { CadColor } from '../types/cad';
+/**
+ * 解析 CAD packed RGB 数值为 [r, g, b] 数组
+ *
+ * rgb 格式:`0xMMBBGGRR`(32-bit)
+ * - bit 0-7: 红色通道 (R)
+ * - bit 8-15: 绿色通道 (G)
+ * - bit 16-23: 蓝色通道 (B)
+ * - bit 24-31: method(颜色模式标识)
+ *
+ * @param packed 32-bit 打包颜色值(含 method 高字节)
+ * @returns [r, g, b] 0-255
+ */
+export declare function parseCadRgb(packed: number): [number, number, number];
+/** 从 rgb 高字节提取 method */
+export declare function extractMethod(rgb: number): number;
+/**
+ * 判断颜色是否为 BYLAYER(从图层继承颜色)
+ *
+ * 两种判断方式(任一满足即为随层):
+ * - index === 256(传统 ACI 方式)
+ * - method === 0xC0(真彩色方式)
+ */
+export declare function isColorByLayer(color: CadColor): boolean;
+/**
+ * 判断颜色是否为 BYBLOCK(从块定义继承颜色)
+ *
+ * 两种判断方式:
+ * - index === 0(传统 ACI 方式)
+ * - method === 0xC1(真彩色方式)
+ */
+export declare function isColorByBlock(color: CadColor): boolean;
+/**
+ * 从 CAD 颜色对象解析为 RGBA,遵循 AutoCAD 颜色优先级规则
+ *
+ * 颜色解析优先级(由高到低):
+ * 1. method=0xC2 → 命名配色书(回退白色)
+ * 2. method=0xC3 → 真彩色 RGB
+ * 3. index 1~255 → ACI 索引色查表
+ * 4. index=0 → BYBLOCK(回退白色)
+ * 5. index=256 → BYLAYER(从 layerColorMap 查找图层颜色)
+ *
+ * @param color CAD 颜色对象
+ * @param layerColorMap 可选,图层ID→颜色的映射(用于 BYLAYER 解析)
+ * @returns [r, g, b, a] 0-255
+ */
+export declare function cadColorToRgba(color: CadColor, defaultColor: number[], layerColorMap?: Record<number, CadColor>): [number, number, number, number];
+/**
+ * 根据图层颜色和当前特征颜色,解析特征最终颜色
+ *
+ * 处理 BYLAYER 情况:如果特征颜色是随层的,
+ * 则在 layerColorMap 中查找对应图层的颜色并递归解析。
+ *
+ * @param featureColor 特征自身的颜色
+ * @param layerId 特征所在图层 ID
+ * @param layerColorMap 图层 ID→颜色 的映射表
+ * @returns [r, g, b, a] 0-255
+ */
+export declare function resolveFeatureColor(featureColor: CadColor, layerId: number, layerColorMap: Record<number, CadColor>, defaultColor: number[]): [number, number, number, number];
+/**
+ * 通过 ACI 索引获取 RGB 颜色值
+ * @param index ACI 颜色索引 (1-255)
+ * @returns [r, g, b] 0-255
+ */
+export declare function aciToRgb(index: number): [number, number, number];

+ 90 - 0
src/components/map/types/utils/coordTransform.d.ts

@@ -0,0 +1,90 @@
+/**
+ * 坐标变换工具
+ *
+ * 负责将 CAD 投影坐标系(如 CGCS2000 高斯-克吕格投影)中的坐标
+ * 转换为 WGS84 经纬度,以便在 MapLibre GL JS 地图上叠加显示。
+ *
+ * ## 策略说明
+ *
+ * CAD 原始坐标是由 librecad 解析生成的,通常是**投影坐标(米)**,
+ * 数值范围例如 X: 37,400,000~37,440,000、Y: 4,330,000~4,335,000。
+ * 本工具提供两种转换方式:
+ *
+ * 1. **proj4 投影转换** — 通过 proj4 定义源/目标投影做精确转换(需安装 proj4 库)
+ * 2. **偏移+缩放** — 将 CAD 数据相对中心对齐到地图中心,适用相对叠加显示
+ *
+ * 当前 CAD 视图使用 OrthographicView(独立于地图),
+ * 因此默认使用 `rawMode=true`,保持原始 CAD 坐标不变。
+ * 坐标变换仅在地图模式下叠加 CAD 点时使用。
+ */
+/**
+ * 计算一组二维点的最小外接矩形
+ * @param points 点数组
+ * @returns {minX, minY, maxX, maxY} 或 null(空数组)
+ */
+export declare function computeBounds(points: {
+    x: number;
+    y: number;
+}[]): {
+    minX: number;
+    minY: number;
+    maxX: number;
+    maxY: number;
+} | null;
+/**
+ * 分析整个 CAD 数据集,计算边界、中心点、跨度等统计信息
+ *
+ * 遍历所有图元(LINE / LWPOLYLINE / HATCH / TEXT / MTEXT),
+ * 收集所有坐标点后求最小外接矩形。
+ *
+ * @param features CAD 图元数组
+ * @returns 统计结果对象
+ */
+export declare function analyzeCadCoords(features: {
+    typename: string;
+    properties: any;
+}[]): {
+    bounds: {
+        minX: number;
+        minY: number;
+        maxX: number;
+        maxY: number;
+    } | null;
+    centerX: number;
+    centerY: number;
+    spanX: number;
+    spanY: number;
+};
+/**
+ * 将单个 CAD 坐标点转换为 WGS84 经纬度
+ *
+ * 通过偏移+缩放方式:将 CAD 数据中心对齐到地图中心,
+ * 保留相对位置关系。适用于在地图模式下叠加显示 CAD 元素。
+ *
+ * @param cadX CAD X 坐标(投影坐标系/米)
+ * @param cadY CAD Y 坐标
+ * @param mapCenterLng 地图中心经度
+ * @param mapCenterLat 地图中心纬度
+ * @param cadCenterX CAD 数据范围中心 X
+ * @param cadCenterY CAD 数据范围中心 Y
+ * @param scale 缩放系数,默认 1e-5(米→度的粗略转换)
+ * @returns [经度, 纬度]
+ */
+export declare function cadToMap(cadX: number, cadY: number, mapCenterLng: number, mapCenterLat: number, cadCenterX: number, cadCenterY: number, scale?: number): [number, number];
+/**
+ * 创建批量坐标转换函数的闭包
+ *
+ * 预先绑定地图中心、CAD 中心和缩放系数,
+ * 返回 (x, y) => [lng, lat] 的转换函数。
+ *
+ * @returns 转换函数
+ */
+export declare function createCadToMapFn(mapCenterLng: number, mapCenterLat: number, cadCenterX: number, cadCenterY: number, scale?: number): (x: number, y: number) => [number, number];
+/**
+ * 默认缩放系数
+ *
+ * 从数据观察 CAD 坐标 X 范围约 7,000(如 37,436,000~37,443,000),
+ * 对应实际距离约 7 千米。1 度 WGS84 ≈ 111 千米,
+ * 因此 7,000 米 ≈ 0.07 度,缩放系数 1e-5 可使跨度大致匹配。
+ */
+export declare const DEFAULT_SCALE = 0.00001;

+ 6 - 0
src/components/map/types/utils/regionAdcodes.d.ts

@@ -0,0 +1,6 @@
+/**
+ * 区县级 adcode 字典(自动生成:从 src/assets/boundaries/*_full.json 提取)
+ * 用途:根据区县名称查找 adcode,供边界加载与 getCityCenter 使用
+ * 当前覆盖省份:山西、陕西
+ */
+export declare const DISTRICT_ADCODE: Record<string, number>;

+ 582 - 0
src/views/vent/cad/dwgViewerTest.vue

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

+ 1 - 1
src/views/vent/monitorManager/hsqHome/fireMonitor.vue

@@ -59,7 +59,7 @@ import CommonTable from './components/CommonTable.vue';
 import AlarmMonitor from './components/AlarmMonitor.vue';
 import EnvironmentMonitor from './components/EnvironmentMonitor.vue';
 import QuickAction from './components/QuickAction.vue';
-import dwgViewer from '../../cad/dwgViewer.vue';
+import dwgViewer from '../../cad/dwgViewerTest.vue';
 import { option, chartConfig, deviceColumns, quickOption, btnOption, alarmOption, chartTempConfig, } from './hsqHome.data';
 import { managesysList, monitorSystem, deviceList, monitorDevice, getManagesysDeviceNum } from './hsqHome.api';
 

Vissa filer visades inte eftersom för många filer har ändrats