高德地图添加电子围栏和老人出来电子围栏之后的报警

This commit is contained in:
2026-06-21 22:56:00 +08:00
parent d1a8d835fa
commit 09256b2808
2 changed files with 708 additions and 208 deletions

View File

@@ -1,64 +1,192 @@
<template>
<div class="map-page">
<!-- 地图初始化加载/错误状态 -->
<!-- 地图初始化状态 -->
<div v-if="loading" class="map-status"> 地图加载中...</div>
<div v-else-if="error" class="map-status map-error"> {{ error }}</div>
<!-- 定位进行中提示 -->
<div v-if="locating" class="map-status map-locating">
{{ locatingSource === 'amap' ? '📡 基站/IP 定位中...' : '🛰 GPS 定位中...' }}
<!-- 🚨 越界报警浮层 -->
<div v-if="alarmActive" class="alarm-overlay" @click="dismissAlarm">
<div class="alarm-box">
<div class="alarm-icon">🚨</div>
<div class="alarm-content">
<div class="alarm-title">电子围栏报警</div>
<div class="alarm-desc">
老人已离开安全区域
<br />
<span v-if="alarmCount > 1">累计越界 {{ alarmCount }} </span>
</div>
<div class="alarm-hint">点击任意处解除警报</div>
</div>
</div>
</div>
<!-- 地图容器 -->
<!-- 围栏内/外状态标签 -->
<div
v-if="hasPerson && hasFence && !isDrawing"
class="fence-status"
:class="alarmActive ? 'fence-status--alarm' : 'fence-status--safe'"
>
{{ alarmActive ? '🚨 老人已越界!' : '✅ 老人在安全区域内' }}
</div>
<!-- 地图容器 -->
<div
ref="containerRef"
class="map-container"
:class="{ 'map-hidden': loading || error }"
/>
<!-- 底部操作栏 -->
<!-- 底部控制栏 -->
<div class="map-controls">
<span class="map-title">
🗺 高德地图
<span v-if="locatingSourceLabel" class="locate-badge">{{ locatingSourceLabel }}</span>
</span>
<!-- 第一行围栏操作 -->
<div class="controls-row">
<span class="map-title">🗺 电子围栏</span>
<div class="map-actions">
<button @click="resetView" :disabled="!mapInstance || locating">📍 回到默认位置</button>
<!-- 绘制按钮 -->
<button
@click="locate"
:disabled="!mapInstance || locating"
class="btn-locate"
:disabled="!mapInstance || isDrawing"
@click="startDrawPolygon"
title="点击地图添加顶点 → 点击首顶点(红色①)闭合 → 或点击「闭合」按钮"
>
<span v-if="locating" class="spinner" />
{{ locating ? '定位中...' : '🎯 我的位置' }}
{{ isDrawing && fenceType === 'polygon' ? '⏳ 绘制中...' : '🔷 绘制围栏' }}
</button>
<button
:disabled="!mapInstance || isDrawing"
@click="startDrawCircle"
title="第一次点击确定圆心,第二次点击确定半径"
>
{{ isDrawing && fenceType === 'circle' ? '⏳ 绘制中...' : '⭕ 圆形围栏' }}
</button>
<!-- 闭合围栏按钮多边形绘制中 + 已添加 3+ 顶点 -->
<button
v-if="isDrawing && fenceType === 'polygon' && drawVertexCount >= 3"
class="btn-finish"
@click="finishDraw"
>
闭合围栏{{ drawVertexCount }}
</button>
<!-- 绘制中 取消 -->
<button
v-if="isDrawing"
class="btn-cancel"
@click="cancelDraw"
>
取消
</button>
<!-- 清除围栏 -->
<button
v-if="hasFence && !isDrawing"
class="btn-danger"
@click="clearFence"
>
🗑 清除围栏
</button>
</div>
</div>
<!-- 绘制提示条正在绘制时显示 -->
<div v-if="isDrawing" class="controls-row draw-hint-row">
<span class="draw-hint">
<template v-if="fenceType === 'polygon'">
🎯 点击地图添加顶点 &nbsp;|&nbsp;
<strong>点击红色 号首顶点</strong> <strong>闭合围栏按钮</strong> 完成 &nbsp;|&nbsp;
右键取消 &nbsp;|&nbsp;
已添加 <strong>{{ drawVertexCount }}</strong> 个点
<span v-if="drawVertexCount >= 3" class="draw-hint-ok"> 可以闭合了</span>
<span v-else class="draw-hint-min">至少还需要 {{ 3 - drawVertexCount }} 个点</span>
</template>
<template v-else-if="fenceType === 'circle'">
🎯 <strong> 1 </strong>点击地图设置圆心 &nbsp;|&nbsp;
<strong> 2 </strong>再次点击设定半径 &nbsp;|&nbsp;
右键取消
</template>
</span>
</div>
<!-- 第二行老人标记操作 -->
<div class="controls-row">
<span class="controls-label">👴 老人控制</span>
<div class="map-actions">
<button
v-if="!hasPerson"
:disabled="!mapInstance || isDrawing"
@click="addPerson"
>
添加老人
</button>
<template v-else>
<!-- 方向键 -->
<div class="dpad">
<button
class="dpad-btn dpad-up"
:disabled="isDrawing"
@click="movePerson('up')"
title="向北移动"
></button>
<button
class="dpad-btn dpad-left"
:disabled="isDrawing"
@click="movePerson('left')"
title="向西移动"
></button>
<button
class="dpad-btn dpad-right"
:disabled="isDrawing"
@click="movePerson('right')"
title="向东移动"
></button>
<button
class="dpad-btn dpad-down"
:disabled="isDrawing"
@click="movePerson('down')"
title="向南移动"
></button>
</div>
<span class="hint-text">或拖拽地图上的 👴 标记</span>
<button class="btn-danger" @click="removePerson">🗑 移除</button>
</template>
</div>
</div>
</div>
<!-- 报警历史侧边信息桌面端 -->
<div v-if="alarmHistory.length > 0" class="alarm-log">
<div class="alarm-log-title">📋 报警记录</div>
<div
v-for="(record, i) in alarmHistory"
:key="i"
class="alarm-log-item"
>
<span class="alarm-log-time">{{ record.time }}</span>
<span class="alarm-log-pos">
[{{ record.position[0].toFixed(4) }}, {{ record.position[1].toFixed(4) }}]
</span>
</div>
<button class="alarm-log-clear" @click="alarmHistory.length = 0; alarmCount = 0">
清空记录
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import { useAmap, loadAMap } from '@/composables/useAmap'
import { onMounted, nextTick, onUnmounted } from 'vue'
import { useAmap } from '@/composables/useAmap'
import { useGeofence } from '@/composables/useGeofence'
// ═══ 地图初始化 ═══
const { containerRef, mapInstance, loading, error, initMap } = useAmap({
center: [116.397428, 39.90923], // 北京天安门
zoom: 12,
pitch: 30,
center: [116.397428, 39.90923],
zoom: 15, // 围栏场景用更大比例尺
pitch: 0, // 2D 俯视图更便于绘制
})
void containerRef
// ─────────────────────────────────────────
// 定位状态
// ─────────────────────────────────────────
const locating = ref(false)
const locatingSource = ref<'gps' | 'amap' | ''>('')
const locatingSourceLabel = ref('')
// ─────────────────────────────────────────
// 地图初始化
// ─────────────────────────────────────────
async function bootstrapMap() {
const map = await initMap()
if (map) {
@@ -67,143 +195,39 @@ async function bootstrapMap() {
}
onMounted(() => bootstrapMap())
// ─────────────────────────────────────────
// 📍 回到默认视图
// ─────────────────────────────────────────
function resetView(): void {
locatingSourceLabel.value = ''
const map = mapInstance.value
if (!map) return
map.setCenter([116.397428, 39.90923])
map.setZoom(12)
}
// ═══ 电子围栏 ═══
const {
fenceType,
isDrawing,
drawVertexCount,
isInside,
alarmActive,
alarmCount,
alarmHistory,
hasFence,
hasPerson,
startDrawPolygon,
startDrawCircle,
finishDraw,
cancelDraw,
clearFence,
addPerson,
removePerson,
movePerson,
dismissAlarm,
destroyGeofence,
} = useGeofence(mapInstance)
// ─────────────────────────────────────────
// 🎯 我的位置 — 双通道定位
// ─────────────────────────────────────────
async function locate(): Promise<void> {
if (locating.value || !mapInstance.value) return
locating.value = true
// ── 通道 1浏览器原生 GPS高精度需 HTTPS / localhost──
const gpsResult = await tryBrowserGPS()
if (gpsResult) {
applyLocation(gpsResult.lng, gpsResult.lat, 16, '🛰️ GPS')
locating.value = false
return
}
// ── 通道 2高德 IP / 基站定位(低精度,但无需用户授权)──
const amapResult = await tryAmapGeolocation()
if (amapResult) {
applyLocation(amapResult.lng, amapResult.lat, 14, '📡 IP定位')
locating.value = false
return
}
// ── 通道 3完全失败 ──
locating.value = false
alert('无法获取您的位置,请检查浏览器定位权限或网络连接')
}
// ─────────────────────────────────────────
// 通道 1浏览器原生 Geolocation API
// ─────────────────────────────────────────
function tryBrowserGPS(): Promise<{ lng: number; lat: number } | null> {
return new Promise((resolve) => {
if (!navigator.geolocation) {
console.log('[定位] 浏览器不支持 Geolocation API降级到高德定位')
resolve(null)
return
}
locatingSource.value = 'gps'
console.log('[定位] 🛰️ 尝试浏览器 GPS 定位...')
navigator.geolocation.getCurrentPosition(
(pos) => {
const { longitude: lng, latitude: lat, accuracy } = pos.coords
console.log(`[定位] ✅ GPS 成功 — 经度:${lng} 纬度:${lat} 精度:${accuracy}`)
resolve({ lng, lat })
},
(err) => {
// GeolocationPositionError.code:
// 1 = PERMISSION_DENIED用户拒绝
// 2 = POSITION_UNAVAILABLE信号不可用
// 3 = TIMEOUT超时
const reason = ['', '用户拒绝授权', '信号不可用', '定位超时'][err.code] || err.message
console.log(`[定位] ❌ GPS 失败 (${reason}),降级到高德定位`)
resolve(null)
},
{
timeout: 8000, // 8 秒超时
enableHighAccuracy: true, // 强制启用 GPS 芯片
maximumAge: 60000, // 允许 60 秒内的缓存(避免每次冷启动 GPS
}
)
})
}
// ─────────────────────────────────────────
// 通道 2高德 IP / 基站定位
// ─────────────────────────────────────────
async function tryAmapGeolocation(): Promise<{ lng: number; lat: number } | null> {
try {
const AMap = await loadAMap()
locatingSource.value = 'amap'
console.log('[定位] 📡 尝试高德 IP 定位...')
return new Promise((resolve) => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, // 混合定位(优先 GPS → Wi-Fi → 基站 → IP
timeout: 6000,
panToLocation: false, // 我们自己控制地图移动
})
geolocation.getCurrentPosition((status, result) => {
if (status === 'complete' && result.position) {
const { lng, lat } = result.position
console.log(
`[定位] ✅ 高德定位成功 — 经度:${lng} 纬度:${lat}`
+ ` 地址:${result.formattedAddress} 精度:${result.accuracy}`
)
resolve({ lng, lat })
} else {
console.log(`[定位] ❌ 高德定位失败: ${result.message}`)
resolve(null)
}
})
})
} catch (err: any) {
console.log(`[定位] ❌ 高德定位插件加载失败: ${err.message}`)
return null
}
}
// ─────────────────────────────────────────
// 将定位结果应用到地图
// ─────────────────────────────────────────
function applyLocation(lng: number, lat: number, zoom: number, label: string): void {
const map = mapInstance.value
if (!map) return
map.setZoomAndCenter(zoom, [lng, lat])
locatingSourceLabel.value = label
// 3 秒后自动隐藏标签
setTimeout(() => {
if (locatingSourceLabel.value === label) {
locatingSourceLabel.value = ''
}
}, 3000)
}
// ═══ 生命周期清理 ═══
onUnmounted(() => {
destroyGeofence()
})
</script>
<style scoped>
/* ============================================================
地图页面布局header(48px) | map(剩余) | controls(52px)
============================================================ */
/* ═══════════════════════════════════════════
地图页面布局
═══════════════════════════════════════════ */
.map-page {
flex: 1;
position: relative;
@@ -213,7 +237,7 @@ function applyLocation(lng: number, lat: number, zoom: number, label: string): v
overflow: hidden;
}
/* ---- 状态提示(加载/错误/定位中)---- */
/* ── 状态浮层 ── */
.map-status {
position: absolute;
top: 50%;
@@ -236,13 +260,7 @@ function applyLocation(lng: number, lat: number, zoom: number, label: string): v
border: 1px solid #fecaca;
}
.map-locating {
color: #2d6cdf;
background: rgba(232, 240, 255, 0.95);
border: 1px solid #93c5fd;
}
/* ---- 地图容器 ---- */
/* ── 地图容器 ── */
.map-container {
flex: 1;
width: 100%;
@@ -253,13 +271,121 @@ function applyLocation(lng: number, lat: number, zoom: number, label: string): v
visibility: hidden;
}
/* ---- 底部操作栏 ---- */
.map-controls {
/* ═══════════════════════════════════════════
🚨 越界报警浮层
═══════════════════════════════════════════ */
.alarm-overlay {
position: absolute;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: space-between;
height: 52px;
padding: 0 20px;
justify-content: center;
background: rgba(255, 0, 0, 0.08);
cursor: pointer;
animation: alarm-bg-pulse 1s ease-in-out infinite;
}
@keyframes alarm-bg-pulse {
0%, 100% { background: rgba(255, 0, 0, 0.04); }
50% { background: rgba(255, 0, 0, 0.12); }
}
.alarm-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 32px 48px;
background: rgba(255, 255, 255, 0.98);
border-radius: 16px;
box-shadow: 0 8px 40px rgba(255, 0, 0, 0.25);
border: 2px solid #FF4D4F;
animation: alarm-box-shake 0.4s ease-in-out;
}
@keyframes alarm-box-shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
}
.alarm-icon {
font-size: 48px;
animation: alarm-icon-pulse 0.6s ease-in-out infinite alternate;
}
@keyframes alarm-icon-pulse {
from { transform: scale(1); }
to { transform: scale(1.15); }
}
.alarm-content {
text-align: center;
}
.alarm-title {
font-size: 22px;
font-weight: 700;
color: #FF4D4F;
margin-bottom: 6px;
}
.alarm-desc {
font-size: 14px;
color: #666;
line-height: 1.6;
}
.alarm-hint {
margin-top: 10px;
font-size: 12px;
color: #aaa;
}
/* ═══════════════════════════════════════════
围栏状态标签
═══════════════════════════════════════════ */
.fence-status {
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
z-index: 800;
padding: 6px 18px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
pointer-events: none;
transition: all 0.3s;
}
.fence-status--safe {
background: rgba(66, 184, 131, 0.12);
color: #2e7d32;
border: 1px solid rgba(66, 184, 131, 0.3);
}
.fence-status--alarm {
background: rgba(255, 77, 79, 0.12);
color: #c62828;
border: 1px solid rgba(255, 77, 79, 0.3);
animation: status-alarm-pulse 0.6s ease-in-out infinite alternate;
}
@keyframes status-alarm-pulse {
from { opacity: 1; }
to { opacity: 0.5; }
}
/* ═══════════════════════════════════════════
底部控制栏(双行)
═══════════════════════════════════════════ */
.map-controls {
display: flex;
flex-direction: column;
background: #fff;
border-top: 1px solid #e8e8e8;
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.04);
@@ -267,32 +393,44 @@ function applyLocation(lng: number, lat: number, zoom: number, label: string): v
z-index: 5;
}
.map-title {
.controls-row {
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
padding: 8px 16px;
gap: 12px;
}
.controls-row + .controls-row {
border-top: 1px solid #f0f0f0;
padding-top: 6px;
padding-bottom: 6px;
}
.map-title {
font-size: 15px;
font-weight: 600;
color: #333;
white-space: nowrap;
}
/* 定位来源标签 */
.locate-badge {
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
background: #e8f5e9;
color: #2e7d32;
.controls-label {
font-size: 13px;
font-weight: 600;
color: #555;
white-space: nowrap;
}
.map-actions {
display: flex;
gap: 10px;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
/* ── 通用按钮 ── */
.map-actions button {
padding: 7px 18px;
padding: 6px 14px;
border: 1px solid #42b883;
border-radius: 6px;
background: #fff;
@@ -300,9 +438,7 @@ function applyLocation(lng: number, lat: number, zoom: number, label: string): v
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.map-actions button:hover:not(:disabled) {
@@ -311,22 +447,243 @@ function applyLocation(lng: number, lat: number, zoom: number, label: string): v
}
.map-actions button:disabled {
opacity: 0.4;
opacity: 0.35;
cursor: not-allowed;
}
/* 定位按钮 loading 动画 */
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.7s linear infinite;
.btn-cancel {
border-color: #faad14 !important;
color: #faad14 !important;
}
@keyframes spin {
to { transform: rotate(360deg); }
.btn-cancel:hover:not(:disabled) {
background: #faad14 !important;
color: #fff !important;
}
.btn-finish {
border-color: #42b883 !important;
background: #42b883 !important;
color: #fff !important;
font-weight: 600;
animation: btn-finish-pulse 0.8s ease-in-out infinite alternate;
}
@keyframes btn-finish-pulse {
from { box-shadow: 0 0 0 rgba(66, 184, 131, 0); }
to { box-shadow: 0 0 12px rgba(66, 184, 131, 0.5); }
}
.btn-finish:hover:not(:disabled) {
background: #38a978 !important;
border-color: #38a978 !important;
}
.btn-danger {
border-color: #FF4D4F !important;
color: #FF4D4F !important;
}
.btn-danger:hover:not(:disabled) {
background: #FF4D4F !important;
color: #fff !important;
}
.hint-text {
font-size: 11px;
color: #aaa;
white-space: nowrap;
}
/* ── 绘制提示条 ── */
.draw-hint-row {
background: #f0faf5;
border-top: 1px solid #d0ede0;
}
.draw-hint {
font-size: 12px;
color: #555;
line-height: 1.6;
}
.draw-hint strong {
color: #42b883;
}
.draw-hint-ok {
color: #2e7d32;
font-weight: 600;
}
.draw-hint-min {
color: #faad14;
}
/* ── 方向键 ── */
.dpad {
display: grid;
grid-template-areas:
". up ."
"left . right"
". down .";
grid-template-columns: repeat(3, 32px);
grid-template-rows: repeat(3, 32px);
gap: 2px;
}
.dpad-btn {
width: 30px;
height: 30px;
padding: 0;
border: 1px solid #d9d9d9;
border-radius: 4px;
background: #fafafa;
color: #555;
font-size: 15px;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.dpad-btn:hover:not(:disabled) {
background: #42b883;
border-color: #42b883;
color: #fff;
}
.dpad-btn:disabled {
opacity: 0.25;
cursor: not-allowed;
}
.dpad-up { grid-area: up; }
.dpad-left { grid-area: left; }
.dpad-right { grid-area: right; }
.dpad-down { grid-area: down; }
/* ═══════════════════════════════════════════
报警历史侧边记录
═══════════════════════════════════════════ */
.alarm-log {
position: absolute;
right: 12px;
bottom: 130px;
z-index: 700;
width: 200px;
max-height: 180px;
overflow-y: auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
padding: 10px;
font-size: 12px;
}
.alarm-log-title {
font-weight: 600;
color: #FF4D4F;
margin-bottom: 6px;
font-size: 13px;
}
.alarm-log-item {
display: flex;
justify-content: space-between;
padding: 3px 0;
border-bottom: 1px solid #f5f5f5;
}
.alarm-log-time {
color: #888;
}
.alarm-log-pos {
color: #555;
font-family: monospace;
font-size: 10px;
}
.alarm-log-clear {
margin-top: 6px;
width: 100%;
padding: 4px 0;
border: 1px solid #e8e8e8;
border-radius: 4px;
background: #fff;
color: #999;
font-size: 11px;
cursor: pointer;
}
.alarm-log-clear:hover {
background: #f5f5f5;
color: #666;
}
</style>
<!-- AMap 自定义 Marker 全局样式不能 scoped因为 Marker DOM AMap 直接注入 -->
<style>
/* ── 普通顶点标记 ── */
.geofence-vertex-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #42b883;
border: 2px solid #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
font-size: 8px;
color: #fff;
text-align: center;
line-height: 12px;
user-select: none;
}
/* ── 首顶点高亮≥3 顶点后出现,提示可以点击闭合)── */
.geofence-first-vertex {
width: 20px;
height: 20px;
border-radius: 50%;
background: #FF4D4F;
border: 3px solid #fff;
box-shadow: 0 0 10px rgba(255, 77, 79, 0.7);
font-size: 11px;
font-weight: 700;
color: #fff;
text-align: center;
line-height: 20px;
user-select: none;
animation: geofence-vertex-pulse 0.8s ease-in-out infinite alternate;
cursor: pointer;
}
@keyframes geofence-vertex-pulse {
from {
box-shadow: 0 0 6px rgba(255, 77, 79, 0.5);
transform: scale(1);
}
to {
box-shadow: 0 0 16px rgba(255, 77, 79, 0.9);
transform: scale(1.2);
}
}
/* ── 老人标记 ── */
.geofence-person-marker {
width: 34px;
height: 34px;
border-radius: 50%;
background: linear-gradient(135deg, #FF6B6B, #FF4D4F);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border: 2px solid #fff;
box-shadow: 0 2px 10px rgba(255, 77, 79, 0.35);
cursor: grab;
user-select: none;
}
</style>

143
src/vite-env.d.ts vendored
View File

@@ -40,6 +40,12 @@ declare namespace AMap {
destroy(): void
on(event: string, handler: (...args: any[]) => void): void
off(event: string, handler: (...args: any[]) => void): void
setStatus(status: Partial<{ doubleClickZoom: boolean; dragEnable: boolean; zoomEnable: boolean }>): void
getStatus(): { doubleClickZoom: boolean; dragEnable: boolean; zoomEnable: boolean }
setDefaultCursor(cursor: string): void
getDefaultCursor(): string
getCenter(): { lng: number; lat: number }
lngLatToContainer(lnglat: { lng: number; lat: number }): { x: number; y: number }
}
class Geocoder {
@@ -97,6 +103,143 @@ declare namespace AMap {
class MarkerClusterer {
constructor(map: Map, markers: any[], opts?: Record<string, any>)
}
// ── 覆盖物基类 ──
class Overlay {
setMap(map: Map | null): void
getMap(): Map | null
destroy(): void
show(): void
hide(): void
}
// ── 点标记 ──
interface MarkerOptions {
position?: [number, number]
content?: string
icon?: string | Icon
offset?: Pixel
draggable?: boolean
zIndex?: number
[key: string]: any
}
class Marker extends Overlay {
constructor(opts?: MarkerOptions)
setPosition(position: [number, number]): void
getPosition(): { lng: number; lat: number }
on(event: string, handler: (...args: any[]) => void): void
off(event: string, handler: (...args: any[]) => void): void
}
// ── 像素偏移 ──
class Pixel {
constructor(x: number, y: number)
}
// ── 图标 ──
interface IconOptions {
size?: [number, number]
image?: string
imageSize?: [number, number]
imageOffset?: [number, number]
}
class Icon {
constructor(opts?: IconOptions)
}
// ── 多边形 ──
interface PolygonOptions {
path?: [number, number][]
fillColor?: string
fillOpacity?: number
strokeColor?: string
strokeWeight?: number
strokeOpacity?: number
strokeStyle?: 'solid' | 'dashed'
zIndex?: number
[key: string]: any
}
class Polygon extends Overlay {
constructor(opts?: PolygonOptions)
setPath(path: [number, number][]): void
getPath(): [number, number][]
}
// ── 折线(绘制围栏时连线预览)──
interface PolylineOptions {
path?: [number, number][]
strokeColor?: string
strokeWeight?: number
strokeOpacity?: number
strokeStyle?: 'solid' | 'dashed'
borderWeight?: number
zIndex?: number
[key: string]: any
}
class Polyline extends Overlay {
constructor(opts?: PolylineOptions)
setPath(path: [number, number][]): void
getPath(): [number, number][]
}
// ── 圆形 ──
interface CircleOptions {
center?: [number, number]
radius?: number
fillColor?: string
fillOpacity?: number
strokeColor?: string
strokeWeight?: number
strokeOpacity?: number
zIndex?: number
[key: string]: any
}
class Circle extends Overlay {
constructor(opts?: CircleOptions)
setCenter(center: [number, number]): void
getCenter(): { lng: number; lat: number }
setRadius(radius: number): void
getRadius(): number
contains(point: [number, number]): boolean
}
// ── 几何计算工具 ──
namespace GeometryUtil {
function isPointInRing(point: { lng: number; lat: number }, path: [number, number][]): boolean
function distance(p1: [number, number], p2: [number, number]): number
}
// ── 鼠标绘制工具 ──
interface MouseToolOptions {
[key: string]: any
}
class MouseTool {
constructor(map: Map, opts?: MouseToolOptions)
polygon(opts?: Record<string, any>): void
circle(opts?: Record<string, any>): void
rectangle(opts?: Record<string, any>): void
close(): void
on(event: string, handler: (...args: any[]) => void): void
off(event: string, handler: (...args: any[]) => void): void
}
// ── 信息窗体 ──
interface InfoWindowOptions {
content?: string
position?: [number, number]
offset?: Pixel
[key: string]: any
}
class InfoWindow extends Overlay {
constructor(opts?: InfoWindowOptions)
open(map: Map, position?: [number, number]): void
close(): void
setContent(content: string): void
}
}
declare module '*.vue' {