ArcGIS JS 基础教程(41):Popup 弹出窗

ArcGIS JS 基础教程(41):Popup 弹出窗

零、写在前面

📌 本系列教程完整目录 :ArcGIS JS 系列基础教程(100个项目常用热门功能)

💡 在线示例 :完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验

🗂️ 专栏导航 :收藏 + 关注,专栏文章第一时间送达

❤️ 一键三连:点赞 + 评论 + 收藏


一、功能介绍

Popup(弹出窗)是 ArcGIS 中点击要素后展示详情信息的标准组件。它既可以是图层自动弹出的「属性卡片」,也可以由代码手动打开。在 5.0 版本 中,视图内置的弹窗本身就是 <arcgis-popup> Web Component ,我们为图层配置 PopupTemplate 后,点击要素时就是由该组件自动渲染,因此完全符合「组件优先」原则(无需改用 Popup 命令式 widget)。

PopupTemplate 的能力包括:

  • 标题 / 内容模板 :支持字段值引用({字段名})、自定义 HTML 文本、字段表格(fields)、媒体图表(media)等
  • 自定义动作(actions):在弹窗右上角追加按钮,点击触发业务回调(如「缩放到」「高亮」「下发指令」)
  • 自动定位:弹窗自动锚定到要素的三维位置

参考:官方 PopupTemplate API | Popup actions 示例

💡 三维要点 :3D 弹窗会自动贴合要素的三维坐标,相机无需切换即可在球体/建筑上方悬浮显示。弹窗内的 fields / text 内容结构在 2D/3D 完全一致。


二、功能实现

2.1 配置 PopupTemplate

为图层(或单个 Graphic)设置 popupTemplate,即可在点击时自动弹出:

javascript 复制代码
popupTemplate: {
    title: "{NAME}",
    content: [
        { type: "fields", fieldInfos: [
            { fieldName: "CATEGORY", label: "类别" },
            { fieldName: "STATUS", label: "运行状态" },
            { fieldName: "VALUE", label: "实时数值" }
        ]},
        { type: "text", text: "<b>提示:</b>点击右侧动作按钮执行联动操作。" }
    ],
    actions: [
        { id: "zoom", label: "缩放到", icon: "zoom-to" },
        { id: "highlight", label: "高亮", icon: "highlight" }
    ]
}

2.2 响应弹窗动作(trigger-action)

javascript 复制代码
view.popup.on("trigger-action", (event) => {
    const action = event.action;
    const feature = view.popup.selectedFeature;
    if (action.id === "zoom") {
        view.goTo(feature.geometry);
    }
});

2.3 程序化打开 / 关闭

javascript 复制代码
view.popup.open({ features: [feature], location: feature.geometry });
view.popup.close();
view.popupEnabled = false;   // 关闭点击自动弹窗

三、功能应用

应用场景 说明
设施属性卡片 点击设备查看名称、状态、实时值
弹窗动作联动 「缩放到」「高亮」或下发业务指令
图表内嵌 media 类型在弹窗展示 ECharts/统计图
程序化弹窗 检索结果列表点击后在地图定位并弹窗
批量要素详情 多个要素命中时弹窗分页展示

四、核心代码

📦 完整代码 已保存至 sample/lesson41_popup.html,可直接在浏览器打开。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第41课:Popup 弹出窗</title>
    <link rel="stylesheet" href="https://js.arcgis.com/5.0/esri/themes/light/main.css">
    <script type="module" src="https://js.arcgis.com/5.0/"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: "Microsoft YaHei", sans-serif; }
        #mapContainer { width: 100vw; height: 100vh; }
        .page-title {
            position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
            background: rgba(255,255,255,0.95); padding: 10px 24px; border-radius: 6px;
            font-size: 18px; font-weight: bold; z-index: 100;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        }
        .control-panel {
            position: absolute; top: 80px; right: 20px;
            background: rgba(255,255,255,0.95); padding: 16px; border-radius: 8px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.15);
            z-index: 100; min-width: 260px;
        }
        .control-panel h3 { margin: 0 0 8px 0; font-size: 14px; color: #333; }
        .btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
        .btn-row button {
            flex: 1; min-width: 100px; padding: 6px 0;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer; font-size: 12px;
        }
        .btn-row button:hover { border-color: #1890ff; color: #1890ff; }
        .btn-row button.on { background: #1890ff; color: white; border-color: #1890ff; }
        .info-card {
            margin-top: 10px; padding: 10px 12px;
            background: #f0f5ff; border-radius: 6px;
            border-left: 3px solid #1890ff; font-size: 12px; line-height: 1.8;
        }
        .info-card .val { font-weight: bold; color: #1890ff; }
        .status-text {
            position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
            background: rgba(0,0,0,0.7); color: white; padding: 8px 20px;
            border-radius: 20px; font-size: 13px; z-index: 100; pointer-events: none;
            white-space: nowrap;
        }
    </style>
</head>
<body>
<h1 class="page-title">第41课:Popup 弹出窗</h1>

<div class="control-panel">
    <h3>💬 弹窗控制</h3>
    <div class="btn-row">
        <button id="btnOpen">📌 弹出首个监测点</button>
        <button id="btnClose">✖️ 关闭弹窗</button>
    </div>
    <div class="btn-row">
        <button id="btnToggle" class="on">🔔 点击弹窗开</button>
    </div>
    <div class="info-card">
        <div>上次动作:<span class="val" id="lastAction">无</span></div>
        <div>当前弹窗:<span class="val" id="popupTitle">无</span></div>
    </div>
</div>

<div class="status-text" id="statusText">Popup | 点击要素查看弹出窗与自定义动作</div>

<div id="mapContainer"></div>

<script type="module">
    const [Map, SceneView, FeatureLayer, GraphicsLayer, Graphic] = await $arcgis.import([
        "@arcgis/core/Map.js",
        "@arcgis/core/views/SceneView.js",
        "@arcgis/core/layers/FeatureLayer.js",
        "@arcgis/core/layers/GraphicsLayer.js",
        "@arcgis/core/Graphic.js"
    ]);
    const getTianditu = await $arcgis.import("https://openlayers.vip/examples/resources/tianditu.js");

    const vecLayers = getTianditu.default({ type: "vec_w" });
    const map = new Map({ basemap: { baseLayers: [vecLayers.base, vecLayers.anno] } });

    const view = new SceneView({
        container: "mapContainer", map: map,
        camera: {
            position: { longitude: 121.498, latitude: 31.205, z: 4000 },
            heading: 20, tilt: 60
        }
    });

    // ===== 监测点图层(带 PopupTemplate) =====
    const monitorLayer = (() => {
        const cats = ["A类", "B类", "C类"];
        const colors = {
            "A类": [227, 74, 51],
            "B类": [56, 135, 220],
            "C类": [60, 180, 110]
        };
        const statuses = ["正常", "预警", "故障"];
        const graphics = [];
        for (let i = 0; i < 80; i++) {
            const cat = cats[i % 3];
            graphics.push(new Graphic({
                geometry: {
                    type: "point",
                    x: 121.46 + Math.random() * 0.08,
                    y: 31.18 + Math.random() * 0.06,
                    z: 80 + Math.random() * 400,
                    spatialReference: { wkid: 4326 }
                },
                attributes: {
                    OBJECTID: i + 1,
                    NAME: "监测点" + (i + 1),
                    CATEGORY: cat,
                    STATUS: statuses[i % 3],
                    VALUE: (Math.random() * 100).toFixed(1)
                }
            }));
        }
        return new FeatureLayer({
            source: graphics,
            objectIdField: "OBJECTID",
            fields: [
                { name: "OBJECTID", type: "oid" },
                { name: "NAME", type: "string" },
                { name: "CATEGORY", type: "string" },
                { name: "STATUS", type: "string" },
                { name: "VALUE", type: "double" }
            ],
            geometryType: "point",
            spatialReference: { wkid: 4326 },
            title: "实时监测点",
            elevationInfo: { mode: "relative-to-ground" },
            renderer: {
                type: "unique-value",
                field: "CATEGORY",
                uniqueValueInfos: cats.map(cat => ({
                    value: cat,
                    symbol: {
                        type: "point-3d",
                        symbolLayers: [{
                            type: "object",
                            resource: { primitive: "sphere" },
                            width: 220, height: 220, depth: 220,
                            material: { color: colors[cat] }
                        }]
                    }
                }))
            },
            // ===== PopupTemplate:标题 + 字段 + 自定义动作 =====
            popupTemplate: {
                title: "{NAME}",
                content: [
                    {
                        type: "fields",
                        fieldInfos: [
                            { fieldName: "CATEGORY", label: "类别" },
                            { fieldName: "STATUS", label: "运行状态" },
                            { fieldName: "VALUE", label: "实时数值" }
                        ]
                    },
                    {
                        type: "text",
                        text: "<b>提示:</b>点击右侧动作按钮执行联动操作。"
                    }
                ],
                actions: [
                    { id: "zoom", label: "缩放到", icon: "zoom-to" },
                    { id: "highlight", label: "高亮", icon: "highlight" }
                ]
            }
        });
    })();

    // ===== 建筑体图层(GraphicsLayer,独立 popupTemplate) =====
    const buildingLayer = (() => {
        const layer = new GraphicsLayer({ title: "重点建筑体" });
        const bases = [
            { x: 121.490, y: 31.230, h: 900, c: [120, 170, 230] },
            { x: 121.495, y: 31.225, h: 1400, c: [230, 160, 70] },
            { x: 121.500, y: 31.220, h: 700, c: [120, 200, 160] },
            { x: 121.485, y: 31.218, h: 1100, c: [200, 120, 200] }
        ];
        bases.forEach((b, idx) => {
            layer.add(new Graphic({
                geometry: {
                    type: "point",
                    x: b.x, y: b.y, z: 0,
                    spatialReference: { wkid: 4326 }
                },
                symbol: {
                    type: "point-3d",
                    symbolLayers: [{
                        type: "object",
                        resource: { primitive: "cube" },
                        width: 350, depth: 350, height: b.h,
                        material: { color: b.c }
                    }]
                },
                attributes: { NAME: "建筑" + (idx + 1), HEIGHT: b.h },
                popupTemplate: {
                    title: "{NAME}",
                    content: [
                        { type: "fields", fieldInfos: [{ fieldName: "HEIGHT", label: "高度(米)" }] }
                    ]
                }
            }));
        });
        return layer;
    })();

    map.addMany([buildingLayer, monitorLayer]);

    // 记录首个监测点要素,供「弹出首个监测点」按钮使用
    let firstFeature = null;
    monitorLayer.queryFeatures().then((res) => {
        firstFeature = res.features[0];
    });

    // ===== 弹窗动作交互(trigger-action) =====
    let highlightHandle = null;
    view.popup.on("trigger-action", (event) => {
        const action = event.action;
        const feature = view.popup.selectedFeature;
        if (!feature) return;

        if (action.id === "zoom") {
            view.goTo(feature.geometry).catch(() => {});
            setLastAction("缩放到:" + feature.attributes.NAME);
        }
        if (action.id === "highlight") {
            if (highlightHandle) highlightHandle.remove();
            view.whenLayerView(feature.layer).then((lv) => {
                highlightHandle = lv.highlight(feature);
            });
            setLastAction("高亮:" + feature.attributes.NAME);
        }
    });

    await view.when();
    // 默认定位:自动框选数据范围,确保初始视角合适
    await monitorLayer.when();
    view.goTo(monitorLayer.fullExtent, { duration: 1500 }).catch(() => {});
    setStatus("Popup 就绪 | 点击场景中的要素查看弹出窗");

    function setStatus(msg) { document.getElementById("statusText").textContent = msg; }
    function setLastAction(msg) { document.getElementById("lastAction").textContent = msg; }

    // 程序化打开弹窗
    document.getElementById("btnOpen").addEventListener("click", () => {
        if (!firstFeature) { setStatus("要素尚未就绪"); return; }
        view.popup.open({
            features: [firstFeature],
            location: firstFeature.geometry
        });
        setStatus("已弹出:" + firstFeature.attributes.NAME);
    });
    // 关闭弹窗
    document.getElementById("btnClose").addEventListener("click", () => {
        view.popup.close();
        setStatus("弹窗已关闭");
    });
    // 点击弹窗开关
    let popupOn = true;
    document.getElementById("btnToggle").addEventListener("click", function () {
        popupOn = !popupOn;
        view.popupEnabled = popupOn;
        this.classList.toggle("on", popupOn);
        setStatus(popupOn ? "点击弹窗已开启" : "点击弹窗已关闭");
    });

    // 弹窗标题变化时同步到面板
    view.popup.watch("title", (title) => {
        document.getElementById("popupTitle").textContent = title || "无";
    });
</script>
</body>
</html>

五、在线示例

🔗 在线体验 :https://southjor.github.io/arcgis-examples/lessons/lesson41.html

操作说明:

  1. 点击场景中任意监测点或建筑,自动弹出 PopupTemplate 定义的属性卡片。
  2. 弹窗右上角的「缩放到 / 高亮」按钮触发 trigger-action 回调,执行相机飞行或要素高亮。
  3. 「弹出首个监测点」通过 view.popup.open({ features }) 程序化打开弹窗。
  4. 「点击弹窗开 / 关」切换 view.popupEnabled,控制是否允许点击自动弹出。

六、关键 API 说明

API 说明
popupTemplate.title 弹窗标题,支持 {字段名} 引用
popupTemplate.content 内容数组(fields / text / media 等)
popupTemplate.actions 弹窗动作按钮({ id, label, icon })
view.popup.on("trigger-action") 点击动作按钮时触发,event.action / view.popup.selectedFeature
view.popup.open({ features, location }) 程序化打开弹窗
view.popupEnabled 是否允许点击自动弹窗

参考链接: PopupTemplate API


七、系列导航

⬅️ 上一篇 :ArcGIS JS 基础教程(40):点击拾取要素 hitTest

➡️ 下一篇 :ArcGIS JS 基础教程(42):属性查询 Query


💡 小贴士 :5.0 中视图自带的弹窗就是 <arcgis-popup> 组件,因此「配置 popupTemplate + 自动弹出」本身已经是组件用法,无需再 new Popup()。若想完全自定义弹窗外观/位置,才需要显式声明 <arcgis-popup> 元素并 popup.view = view,但这种深度定制场景较少。

相关推荐
喆星时瑜9 天前
ArcGIS Pro 基础制图教程
arcgis·arcgis pro·地图制图·arcgis pro 制图
玩大数据的龙威13 天前
农经权二轮延包—固定比例尺不定图幅的公示图批量生成
python·arcgis·二轮延包公示图
非科班Java出身GISer22 天前
ArcGIS JS 基础教程(38):StreamLayer 实时流数据图层
arcgis·arcgis js·arcgis js 流图层·streamlayer·arcgis 流数据图层·arcgis 实时流数据图层
赵钰老师23 天前
基于ArcGIS Pro、R、INVEST等多技术融合下生态系统服务权衡与协同动态分析
python·arcgis·数据分析·r语言
梦想的初衷~24 天前
MaxEnt 物种分布建模学习手册|理论 + R 语言 + ArcGIS 全套实操教程
arcgis·环境变量·r 语言·maxent 模型·物种分布建模·生态学研究·科研教程
jiushidt1 个月前
ArcGIS Pro Add‑in 打包流程详解
arcgis·c#·.net·arcgispro
蓝悦无人机1 个月前
《地面站软件开发》-第2章ArcGIS Maps SDK for Qt 改变视角
arcgis·上位机·地面站·qml·qt6·qt quick·天地图
bug嘛我经常写1 个月前
MIKE21结合ArcGIS制作随时间变化的降水文件(.dfs2)-笔记
经验分享·笔记·arcgis
paiooo1 个月前
用 XML 保存 LLM 会话得到高可读性会话
arcgis