> **本文回答**：`vjmap.ContextMenu` 右键菜单用法 —— 地图 / 覆盖物 / Marker / Draw。

## 一、ContextMenu 基础

```js
new vjmap.ContextMenu({
  event: mouseEvent.originalEvent,  // 鼠标事件对象
  theme: "dark",                     // "dark" | "light"
  width: "250px",
  items: [...]
});
```

## 二、菜单项类型

| type | 说明 |
|---|---|
| `normal`（省略 type）| 普通菜单项 |
| `seperator` | 分割线 |
| `custom` | 自定义 HTML `markup` |
| `multi` | 横向多按钮（"不倾斜/不旋转/全图"） |
| `submenu` | 子菜单（点击弹出） |
| `hovermenu` | 悬浮子菜单 |
| `Button` | 按钮型子项（d.ts 联合成员，用法同 `normal`） |

## 三、地图右键菜单

```js
map.setMenu(event => {
  return new vjmap.ContextMenu({
    event: event.originalEvent,
    theme: "dark",
    width: "250px",
    items: [
      { type: "custom", markup: "<span>菜单右键功能演示</span>" },
      { type: "multi", items: [
        { label: "全图", onClick: () => map.fitMapBounds() },
        { label: "不旋转", onClick: () => map.setBearing(0) },
        { label: "不倾斜", onClick: () => map.setPitch(0) }
      ]},
      { label: "获取此位置坐标", onClick: () => {
        const p = map.fromLngLat(event.lngLat);
        console.log(p.x, p.y);
      }},
      { type: "seperator" },
      { type: "submenu", label: "地图缩放", items: [
        { label: "放大一级", onClick: () => map.setZoom(map.getZoom() + 1) },
        { label: "缩小一级", onClick: () => map.setZoom(map.getZoom() - 1) },
        { label: "飞至此位置", onClick: () =>
          map.flyTo({ center: event.lngLat, pitch: 60, zoom: 5 })
        }
      ]}
    ]
  });
});
```

## 四、覆盖物图层右键菜单

```js
// 先给 polygon 默认菜单
map.setMenu(event => { /* 地图菜单 */ });

// 再给覆盖物菜单（key = "overlay"）
map.setMenu(event => {
  const features = map.queryRenderedFeatures(event.point, {
    layers: [polygon.layerId]
  });
  if (features.length === 0) return null;   // 返回 null → 继续弹地图菜单
  return new vjmap.ContextMenu({
    event: event.originalEvent,
    items: [
      { label: "查看信息", onClick: () => console.log(features[0].properties) },
      { label: "删除", onClick: () => {
        const d = polygon.getData();
        d.features = d.features.filter(f => f.id != features[0].properties.id);
        polygon.setData(d);
      }}
    ]
  });
}, "overlay");    // key 可以自定义，多个 setMenu 按 key 倒序匹配
```

`setMenu(fn, key)`：同一 map 可以挂多个；按 **key 倒序匹配**，空 key 排最后。回调返回 `null` 时继续下一级。

语义（map.ts）：

- 回调不在 `contextmenu` DOM 事件上触发，而是在 **mousedown + mouseup 且 `originalEvent.button === 2`**（右键单击序列，`isClick` 判定位移/时长）时依次调用——合成 `contextmenu` 事件无效；打开前 fire `contextMenuPreOpen`，成功打开后 fire `contextMenuOpened`。
- `setMenu()` / `setMenu(null)`（不传 key）会**摘掉内部的 mousedown/mouseup 监听**，之后右键不再弹任何菜单；但**注册表不清空**（源码 `if (menu)` 跳过赋值）——下一次 `setMenu(fn, key)` 重新挂监听时，之前所有 key 的回调全部复活。只移除某个 key 用 `map.setMenu(() => null, key)`（覆盖为恒返回 null 的回调）。
- `map.isInteracting()` 为 true 时回调不触发，除非设内部开关 `_menu_isInteracting_open = true`。

## 五、Marker 右键菜单

Marker 是 DOM，不走地图 `setMenu`；自己监听：

```js
marker.getElement().addEventListener("mousedown", event => {
  if (event.button !== 2) return;
  event.preventDefault();
  event.stopPropagation();
  new vjmap.ContextMenu({
    event,
    theme: "dark",
    items: [
      { type: "custom", markup: `<span>Marker 菜单</span>` },
      { label: "查看信息", onClick: () => console.log(marker.getLngLat()) }
    ]
  });
});
```

## 六、Draw 工具右键菜单

```js
const draw = new vjmap.Draw.Tool();
map.addControl(draw, "top-right");

// simple_select 模式选中实体时
map.on("draw.simpleselect.contextmenu", e => {
  if (e.styleId !== draw.options.styleId) return;
  const feature = e.event.featureTarget;
  if (!feature) return;
  const selectIds = draw.getSelectedIds() || [];
  if (!selectIds.includes(feature.properties.id)) return;
  e.event.originalEvent.preventDefault();
  e.event.originalEvent.stopPropagation();

  new vjmap.ContextMenu({
    event: e.event.originalEvent,
    items: [
      { label: "删除", onClick: () => draw.delete(feature.properties.id) }
    ]
  });
});
```

类似还有 `draw.directselect.contextmenu`（夹点编辑模式）——其载荷只有 `{mode, state, event}`、**没有 `styleId`**，过滤实例用 `e.state.options.styleId !== draw.options.styleId`。

## 七、菜单项属性

```ts
{
  type?: "custom" | "multi" | "Button" | "seperator" | "submenu" | "hovermenu" | "normal";
  markup?: string;       // type=custom 的 HTML
  items?: [...];         // type=multi/submenu/hovermenu 的子项
  label?: string;
  shortcut?: string;     // 快捷键提示
  enabled?: boolean;
  cssIcon?: string;      // css 类名图标
  icon?: string;         // 图标 URL / base64
  onClick?: () => void;  // 调用时不传任何参数；需要的上下文用闭包捕获（如 setMenu 回调里的 event）
}
```

## 八、常见坑

1. **用了 `event.originalEvent`**：`map.setMenu` 回调里拿到的是 vjmap 封装事件，弹菜单要传 `originalEvent`；`draw.*.contextmenu` 回调里拿到的是 `e.event.originalEvent`。
2. **菜单溢出屏幕** → 设置 `innerWidth` / `innerHeight` 为容器大小：
   ```js
   const { width, height } = map.getContainer().getBoundingClientRect();
   new vjmap.ContextMenu({ event, innerWidth: width, innerHeight: height, ... });
   ```
3. **Marker 菜单不弹出** → 忘了 `preventDefault()`，浏览器原生菜单先弹。
4. **多个 setMenu 互相覆盖** → 传不同的 key。
