Appearance
MapMarkers 标记点
地图标记点组件,用于在地图上渲染多个点位。
基本用法
将 HxMapMarkers 嵌套在 HxMap 内使用,通过 markers 属性传入点位数据。默认使用 circle(圆形)形状渲染。
基础标记点
<template>
<ClientOnly>
<div class="map-markers-verify">
<p class="overall-desc">
HxMapMarkers 功能验证示例,验证组件的核心功能点:独立使用、弹窗交互、动态数据更新、样式动态切换、点击事件等。
</p>
<!-- ==================== 功能验证区 ==================== -->
<div class="verify-section">
<h4>▎验证一:独立使用(无 Cluster)</h4>
<p class="scene-desc">
HxMapMarkers 不嵌套在 HxMapCluster 内,独立创建自己的 VectorSource。
<span v-if="independentReady" class="success-badge">✓ 已就绪</span>
</p>
<div class="control-bar">
<button @click="addRandomMarker" class="ctrl-btn">添加随机点</button>
<button @click="removeLastMarker" class="ctrl-btn">删除最后一点</button>
<button @click="resetMarkers" class="ctrl-btn">重置</button>
<span class="count-label">当前点数:{{ independentMarkers.length }}</span>
</div>
<div class="map-wrapper">
<hx-map
:center="{ lon: 112.5, lat: 31.0 }"
:zoom="7"
:height="350"
@map-click="onIndependentMapClick"
>
<hx-map-markers
:markers="independentMarkers"
:marker-style="currentStyle"
@marker-click="onMarkerClick"
>
<hx-map-popup
v-if="showPopup"
:render="popupRender"
:offset="[0, -currentIconSize / 2 - 10]"
:show-close="true"
/>
</hx-map-markers>
</hx-map>
</div>
<!-- 点击日志 -->
<div v-if="clickLogs.length" class="log-panel">
<div class="log-header">
<span>点击事件日志</span>
<button @click="clickLogs = []">清除</button>
</div>
<div class="log-content">
<div v-for="(log, idx) in clickLogs.slice(-5)" :key="idx" class="log-item">
<span class="log-time">{{ log.time }}</span>
<span class="log-type" :class="log.type">{{ log.label }}</span>
<span class="log-msg">{{ log.msg }}</span>
</div>
</div>
</div>
</div>
<!-- ==================== 样式切换验证 ==================== -->
<div class="verify-section">
<h4>▎验证二:样式动态切换</h4>
<p class="scene-desc">
实时切换标记样式,验证 markerStyle 响应式更新。
</p>
<div class="style-options">
<div class="style-row">
<label>形状:</label>
<button
v-for="s in shapeOptions"
:key="s.value"
:class="['style-btn', { active: currentStyle.shape === s.value }]"
@click="currentStyle.shape = s.value"
>
{{ s.label }}
</button>
</div>
<div class="style-row">
<label>颜色:</label>
<button
v-for="c in colorOptions"
:key="c"
:class="['color-btn', { active: currentStyle.color === c }]"
:style="{ backgroundColor: c }"
@click="currentStyle.color = c"
/>
</div>
<div class="style-row">
<label>尺寸:</label>
<input
type="range"
v-model.number="currentIconSize"
min="16"
max="48"
class="size-slider"
/>
<span class="size-value">{{ currentIconSize }}px</span>
</div>
</div>
<div class="map-wrapper">
<hx-map :center="{ lon: 112.5, lat: 31.0 }" :zoom="7" :height="280">
<hx-map-markers
:markers="styleTestMarkers"
:marker-style="currentStyle"
/>
</hx-map>
</div>
</div>
<!-- ==================== 图标 URL 验证 ==================== -->
<div class="verify-section">
<h4>▎验证三:图标 URL(支持 item 级别覆盖)</h4>
<p class="scene-desc">
item.iconUrl 优先级高于 markerStyle.iconUrl,验证多源图标混合展示。
</p>
<div class="map-wrapper">
<hx-map :center="{ lon: 112.5, lat: 31.0 }" :zoom="7" :height="300">
<hx-map-markers
:markers="iconUrlMarkers"
:marker-style="{
iconUrl: 'https://cdn-icons-png.flaticon.com/32/456/456283.png',
iconSize: [32, 32],
}"
/>
</hx-map>
</div>
<div class="legend">
<span class="legend-item">🏛️ 武汉(item.iconUrl 独立图标)</span>
<span class="legend-item">🔵 其他(markerStyle.iconUrl 兜底)</span>
</div>
</div>
<!-- ==================== 自定义渲染验证 ==================== -->
<div class="verify-section">
<h4>▎验证四:自定义渲染函数</h4>
<p class="scene-desc">
item.render 优先级最高,可完全自定义标记点外观。
</p>
<div class="map-wrapper">
<hx-map :center="{ lon: 112.5, lat: 31.0 }" :zoom="7" :height="300">
<hx-map-markers
:markers="renderMarkers"
:marker-style="{ shape: 'circle', color: '#909399', iconSize: [28, 28] }"
/>
</hx-map>
</div>
<div class="legend">
<span class="legend-item">① 武汉(数字徽章)</span>
<span class="legend-item">🎯 黄石(靶标样式)</span>
<span class="legend-item">⚠️ 十堰(警告三角)</span>
<span class="legend-item">🔵 宜昌-襄阳(circle 兜底)</span>
</div>
</div>
<!-- ==================== 聚合模式验证 ==================== -->
<div class="verify-section">
<h4>▎验证五:嵌套在 Cluster 内使用</h4>
<p class="scene-desc">
HxMapMarkers 嵌套在 HxMapCluster 内时,从 ClusterContext 获取 clusterSource 并设置样式回调。
</p>
<div class="control-bar">
<label>
<span>聚合距离</span>
<input type="range" v-model.number="clusterDistance" min="20" max="100" class="cluster-slider" />
<span>{{ clusterDistance }}px</span>
</label>
</div>
<div class="map-wrapper">
<hx-map :center="{ lon: 112.5, lat: 31.0 }" :zoom="7" :height="300">
<hx-map-cluster
:markers="clusterMarkers"
:distance="clusterDistance"
@cluster-click="onClusterClick"
>
<hx-map-markers
:markers="[]"
:marker-style="{ shape: 'circle', color: '#409eff', iconSize: [24, 24] }"
/>
</hx-map-cluster>
</hx-map>
</div>
<div class="legend">
<span class="legend-item">聚合模式下 HxMapMarkers 的 markers prop 应传空数组</span>
</div>
</div>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
import { ref, reactive, computed, h } from 'vue'
import { HxMap, HxMapMarkers, HxMapCluster, HxMapPopup, registerMapMarkerShapes } from '@whhx/ui'
import type { MapMarkerItem, MarkerStyle } from '@whhx/ui'
// 注册自定义形状
registerMapMarkerShapes([
{
name: 'map-marker',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"/></svg>`,
defaultColor: '#409eff',
},
{
name: 'star',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/></svg>`,
defaultColor: '#f5a623',
},
])
// ==================== 验证一:独立使用 ====================
const independentReady = ref(false)
const showPopup = ref(true)
const independentMarkers = ref<MapMarkerItem[]>([
{ id: 1, lon: 114.31, lat: 30.52, name: '武汉市', type: '省会' },
{ id: 2, lon: 115.03, lat: 29.99, name: '黄石市', type: '地级市' },
{ id: 3, lon: 111.47, lat: 32.65, name: '十堰市', type: '地级市' },
{ id: 4, lon: 111.29, lat: 30.69, name: '宜昌市', type: '地级市' },
{ id: 5, lon: 112.14, lat: 32.01, name: '襄阳市', type: '地级市' },
])
const clickLogs = ref<Array<{ time: string; type: string; label: string; msg: string }>>([])
function addLog(type: string, label: string, msg: string) {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
clickLogs.value.push({ time, type, label, msg })
}
function addRandomMarker() {
const id = Date.now()
const lon = 110 + Math.random() * 8
const lat = 29 + Math.random() * 5
independentMarkers.value.push({
id,
lon,
lat,
name: `随机点_${id}`,
type: '动态添加',
})
addLog('add', '添加', `新增点位 (${lon.toFixed(2)}, ${lat.toFixed(2)})`)
}
function removeLastMarker() {
if (independentMarkers.value.length > 0) {
const removed = independentMarkers.value.pop()
addLog('remove', '删除', `删除 ${removed?.name || ''}`)
}
}
function resetMarkers() {
independentMarkers.value = [
{ id: 1, lon: 114.31, lat: 30.52, name: '武汉市', type: '省会' },
{ id: 2, lon: 115.03, lat: 29.99, name: '黄石市', type: '地级市' },
{ id: 3, lon: 111.47, lat: 32.65, name: '十堰市', type: '地级市' },
{ id: 4, lon: 111.29, lat: 30.69, name: '宜昌市', type: '地级市' },
{ id: 5, lon: 112.14, lat: 32.01, name: '襄阳市', type: '地级市' },
]
clickLogs.value = []
addLog('reset', '重置', '恢复到初始数据')
}
function onIndependentMapClick(coordinate: [number, number]) {
addLog('map', '地图点击', `坐标: (${coordinate[0].toFixed(2)}, ${coordinate[1].toFixed(2)})`)
}
function onMarkerClick(item: MapMarkerItem) {
addLog('marker', '标记点击', `${item.name} (${item.lon}, ${item.lat})`)
}
function popupRender(item: MapMarkerItem) {
return h('div', { class: 'verify-popup' }, [
h('div', { class: 'popup-title' }, item.name),
h('div', { class: 'popup-info' }, [
h('span', { class: 'info-label' }, '类型:'),
h('span', { class: 'info-value' }, item.type),
]),
h('div', { class: 'popup-info' }, [
h('span', { class: 'info-label' }, '坐标:'),
h('span', { class: 'info-value' }, `${item.lon.toFixed(2)}, ${item.lat.toFixed(2)}`),
]),
])
}
// ==================== 验证二:样式切换 ====================
const currentStyle = reactive<MarkerStyle>({
shape: 'circle',
color: '#409eff',
iconSize: [24, 24],
})
const currentIconSize = ref(24)
const shapeOptions = [
{ label: '圆形', value: 'circle' },
{ label: '地图标记', value: 'map-marker' },
{ label: '星形', value: 'star' },
]
const colorOptions = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399']
const styleTestMarkers: MapMarkerItem[] = [
{ id: 1, lon: 114.31, lat: 30.52, name: '武汉' },
{ id: 2, lon: 115.03, lat: 29.99, name: '黄石' },
{ id: 3, lon: 111.47, lat: 32.65, name: '十堰' },
{ id: 4, lon: 111.29, lat: 30.69, name: '宜昌' },
{ id: 5, lon: 112.14, lat: 32.01, name: '襄阳' },
]
// 响应 iconSize 变化
import { watch } from 'vue'
watch(currentIconSize, (val) => {
currentStyle.iconSize = [val, val]
})
// ==================== 验证三:图标 URL ====================
const iconUrlMarkers: MapMarkerItem[] = [
{
id: 1,
lon: 114.31,
lat: 30.52,
name: '武汉',
iconUrl: 'https://cdn-icons-png.flaticon.com/32/684/684908.png',
iconSize: [32, 32],
iconAnchor: [0.5, 0.5],
},
{ id: 2, lon: 115.03, lat: 29.99, name: '黄石' },
{ id: 3, lon: 111.47, lat: 32.65, name: '十堰' },
{ id: 4, lon: 111.29, lat: 30.69, name: '宜昌' },
{ id: 5, lon: 112.14, lat: 32.01, name: '襄阳' },
]
// ==================== 验证四:自定义渲染 ====================
const renderMarkers: MapMarkerItem[] = [
// item.render:数字徽章
{
id: 1,
lon: 114.31,
lat: 30.52,
name: '武汉',
render: () =>
h('div', {
style: {
width: '32px',
height: '32px',
background: 'radial-gradient(circle, #409eff, #2c6fdb)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '14px',
fontWeight: 'bold',
boxShadow: '0 2px 8px rgba(64,158,255,0.6)',
border: '2px solid #fff',
},
}, '1'),
},
// item.render:靶标样式
{
id: 2,
lon: 115.03,
lat: 29.99,
name: '黄石',
render: () =>
h('div', {
style: {
width: '32px',
height: '32px',
background:
'radial-gradient(circle, #e74c3c 20%, #fff 20%, #fff 40%, #e74c3c 40%, #e74c3c 60%, #fff 60%, #fff 80%, #e74c3c 80%)',
borderRadius: '50%',
boxShadow: '0 2px 4px rgba(0,0,0,0.3)',
border: '2px solid #c0392b',
},
}),
},
// item.render:警告三角
{
id: 3,
lon: 111.47,
lat: 32.65,
name: '十堰',
render: () =>
h('div', {
style: {
width: 0,
height: 0,
borderLeft: '14px solid transparent',
borderRight: '14px solid transparent',
borderBottom: '24px solid #e6a23c',
filter: 'drop-shadow(0 2px 4px rgba(230,162,60,0.5))',
},
}),
},
// 使用 markerStyle 兜底
{ id: 4, lon: 111.29, lat: 30.69, name: '宜昌' },
{ id: 5, lon: 112.14, lat: 32.01, name: '襄阳' },
]
// ==================== 验证五:聚合模式 ====================
const clusterDistance = ref(50)
const clusterMarkers: MapMarkerItem[] = [
{ id: 1, lon: 114.31, lat: 30.52, name: '武汉市', type: '省会' },
{ id: 2, lon: 114.35, lat: 30.55, name: '武昌区', type: '区县' },
{ id: 3, lon: 114.28, lat: 30.58, name: '汉口区', type: '区县' },
{ id: 4, lon: 115.03, lat: 29.99, name: '黄石市', type: '地级市' },
{ id: 5, lon: 115.05, lat: 30.02, name: '大冶市', type: '区县' },
{ id: 6, lon: 111.47, lat: 32.65, name: '十堰市', type: '地级市' },
{ id: 7, lon: 111.50, lat: 32.68, name: '茅箭区', type: '区县' },
{ id: 8, lon: 111.29, lat: 30.69, name: '宜昌市', type: '地级市' },
{ id: 9, lon: 111.31, lat: 30.72, name: '西陵区', type: '区县' },
{ id: 10, lon: 112.14, lat: 32.01, name: '襄阳市', type: '地级市' },
]
function onClusterClick(info: any) {
console.log('[Verify] Cluster click:', info)
}
</script>
<style scoped>
.map-markers-verify {
display: flex;
flex-direction: column;
gap: 24px;
}
.overall-desc {
margin: 0;
font-size: 14px;
color: #606266;
line-height: 1.6;
}
.verify-section {
padding: 16px;
background: #fafafa;
border: 1px solid #ebeef5;
border-radius: 8px;
}
.verify-section h4 {
margin: 0 0 8px 0;
font-size: 15px;
color: #303133;
}
.scene-desc {
margin: 0 0 12px 0;
font-size: 13px;
color: #909399;
line-height: 1.5;
}
.success-badge {
display: inline-block;
padding: 2px 8px;
background: #67c23a;
color: #fff;
border-radius: 4px;
font-size: 12px;
margin-left: 8px;
}
/* 控制栏 */
.control-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.ctrl-btn {
padding: 4px 12px;
font-size: 12px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.ctrl-btn:hover {
background: #ecf5ff;
border-color: #409eff;
color: #409eff;
}
.count-label {
font-size: 12px;
color: #606266;
margin-left: 8px;
}
/* 地图容器 */
.map-wrapper {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
/* 点击日志 */
.log-panel {
margin-top: 12px;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
font-size: 12px;
}
.log-header button {
padding: 2px 8px;
font-size: 11px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 3px;
cursor: pointer;
}
.log-content {
max-height: 120px;
overflow-y: auto;
}
.log-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 10px;
font-size: 11px;
border-bottom: 1px solid #f0f0f0;
}
.log-item:last-child {
border-bottom: none;
}
.log-time {
color: #909399;
font-family: monospace;
}
.log-type {
padding: 1px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 500;
}
.log-type.add { background: #f0f9eb; color: #67c23a; }
.log-type.remove { background: #fef0f0; color: #f56c6c; }
.log-type.reset { background: #ecf5ff; color: #409eff; }
.log-type.map { background: #fdf6ec; color: #e6a23c; }
.log-type.marker { background: #f0f9eb; color: #67c23a; }
.log-msg {
color: #606266;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 样式选项 */
.style-options {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
padding: 12px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.style-row {
display: flex;
align-items: center;
gap: 8px;
}
.style-row label {
font-size: 13px;
color: #606266;
min-width: 50px;
}
.style-btn {
padding: 4px 10px;
font-size: 12px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.style-btn:hover {
border-color: #409eff;
}
.style-btn.active {
background: #409eff;
border-color: #409eff;
color: #fff;
}
.color-btn {
width: 24px;
height: 24px;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.color-btn:hover {
transform: scale(1.1);
}
.color-btn.active {
border-color: #303133;
box-shadow: 0 0 0 2px #fff, 0 0 0 4px currentColor;
}
.size-slider {
width: 100px;
cursor: pointer;
}
.size-value {
font-size: 12px;
color: #606266;
min-width: 40px;
}
.cluster-slider {
width: 100px;
cursor: pointer;
}
/* 图例 */
.legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 8px;
padding: 6px 12px;
background: #f5f7fa;
border-radius: 4px;
font-size: 12px;
}
.legend-item {
font-weight: 500;
}
</style>
<style lang="scss">
/* 验证弹窗样式(全局) */
.verify-popup {
min-width: 140px;
font-size: 13px;
}
.popup-title {
font-weight: 600;
font-size: 14px;
color: #303133;
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.popup-info {
display: flex;
justify-content: space-between;
padding: 3px 0;
.info-label {
color: #909399;
}
.info-value {
color: #606266;
font-weight: 500;
}
}
</style>
建筑类型点位展示
<template>
<ClientOnly>
<div class="building-types-demo">
<p class="overall-desc">
多种建筑类型点位展示示例,使用不同的 shape
区分建筑类型,支持动态显隐控制。
</p>
<!-- 控制面板 -->
<div class="control-panel">
<div class="control-header">
<span class="control-title">类型显隐控制</span>
<div class="control-actions">
<button class="ctrl-btn" @click="showAll">显示全部</button>
<button class="ctrl-btn" @click="hideAll">隐藏全部</button>
<button class="ctrl-btn" @click="toggleAll">切换</button>
</div>
</div>
<div class="type-toggles">
<button
v-for="type in buildingTypes"
:key="type.id"
:class="['type-btn', { active: visibleTypes.has(type.id) }]"
:style="{ '--type-color': type.color }"
@click="toggleType(type.id)"
>
<span class="type-icon" v-html="type.icon"></span>
<span class="type-name">{{ type.name }}</span>
<span class="type-count">({{ getVisibleCount(type.id) }})</span>
</button>
</div>
<div class="legend">
<span class="legend-label">图例:</span>
<span
v-for="type in buildingTypes"
:key="type.id"
class="legend-item"
:style="{ color: type.color }"
>
<span v-html="type.icon" class="legend-icon"></span>
{{ type.name }}
</span>
</div>
</div>
<!-- 地图展示 -->
<div class="map-wrapper">
<hx-map :center="{ lon: 116.4, lat: 39.9 }" :zoom="12" :height="500">
<hx-map-markers
v-for="type in buildingTypes"
:key="type.id"
:markers="visibleTypes.has(type.id) ? type.markers : []"
:marker-style="{
shape: type.shape,
color: type.color,
iconSize: [28, 28],
}"
>
<hx-map-popup
:render="popupRender"
:offset="[0, -24]"
:show-close="true"
/>
</hx-map-markers>
</hx-map>
</div>
<!-- 统计信息 -->
<div class="stats-panel">
<div class="stats-title">点位统计</div>
<div class="stats-grid">
<div v-for="type in buildingTypes" :key="type.id" class="stat-item">
<span class="stat-icon" v-html="type.icon"></span>
<span class="stat-name">{{ type.name }}</span>
<span class="stat-count"
>{{ getVisibleCount(type.id) }} / {{ type.markers.length }}</span
>
<div class="stat-bar">
<div
class="stat-bar-fill"
:style="{
width: `${(getVisibleCount(type.id) / type.markers.length) * 100}%`,
backgroundColor: type.color,
}"
></div>
</div>
</div>
</div>
</div>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
import { ref, h } from "vue";
import {
HxMap,
HxMapMarkers,
HxMapPopup,
registerMapMarkerShapes,
} from "@whhx/ui";
import type { MapMarkerItem } from "@whhx/ui";
// 注册建筑类型对应的形状
registerMapMarkerShapes([
{
name: "hospital",
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-1 11h-4v4h-4v-4H6v-4h4V6h4v4h4v4z" fill="currentColor"/>
</svg>`,
defaultColor: "#f56c6c",
},
{
name: "school",
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9L12 3zm6.82 6L12 12.72 5.18 9 12 5.28 18.82 9zM17 15.99l-5 2.73-5-2.73v-3.72L12 15l5-2.73v3.72z" fill="currentColor"/>
</svg>`,
defaultColor: "#409eff",
},
{
name: "gas-station",
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M19.77 7.23l.01-.01-3.72-3.72L15 4.56l2.11 2.11c-.94.36-1.61 1.26-1.61 2.33 0 1.38 1.12 2.5 2.5 2.5.36 0 .69-.08 1-.21v7.21c0 .55-.45 1-1 1s-1-.45-1-1V14c0-1.1-.9-2-2-2h-1V5c0-1.1-.9-2-2-2H6c-1.1 0-2 .9-2 2v16h10v-7.5h1.5v5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V9c0-.69-.28-1.32-.73-1.77zM12 10H6V5h6v5z" fill="currentColor"/>
</svg>`,
defaultColor: "#e6a23c",
},
{
name: "shopping-mall",
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M18 6V4l-2-2H4v16h8v2H4c-1.1 0-2-.9-2-2V4h2v2h2V4h2v2h2V4h2v2h2V4h2v2z" fill="currentColor"/>
<path d="M20 8H4V6h16v2zM20 12H4v-2h16v2zM4 16h8v8H4v-8z" fill="currentColor"/>
<path d="M14 18v4h-4v-4h4m2-2h-8v8c0 1.1-.9 2-2 2v0c1.1 0 2-.9 2-2v-4h2v4c0 .55-.45 1-1 1v0c-.55 0-1-.45-1-1h-2v-4h8v-2z" fill="currentColor"/>
</svg>`,
defaultColor: "#9c27b0",
},
{
name: "bank",
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M4 10h3v7H4v-7zm6.5 0h3v7h-3v-7zM2 19h20v3H2v-3zm15-9h3v7h-3v-7zm-5-9L2 6v2h20V6l-10-5z" fill="currentColor"/>
</svg>`,
defaultColor: "#795548",
},
{
name: "park",
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M17 12h2L12 2 5.05 6.14 12 18l1-5h-4V8l2-5-1.64 3.86L12 10l4.64-3.14L15 8v5h-4l2 5z" fill="currentColor"/>
</svg>`,
defaultColor: "#67c23a",
},
{
name: "hotel",
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M7 13c1.66 0 3-1.34 3-3S8.66 7 7 7s-3 1.34-3 3 1.34 3 3 3zm12-6h-8v7H3V5H1v15h2v-3h18v3h2v-9c0-2.21-1.79-4-4-4z" fill="currentColor"/>
</svg>`,
defaultColor: "#00bcd4",
},
{
name: "restaurant",
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z" fill="currentColor"/>
</svg>`,
defaultColor: "#ff7043",
},
]);
// 建筑类型配置
interface BuildingType {
id: string;
name: string;
color: string;
shape: string;
icon: string;
markers: MapMarkerItem[];
}
const buildingTypes: BuildingType[] = [
{
id: "hospital",
name: "医院",
color: "#f56c6c",
shape: "hospital",
icon: `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-1 11h-4v4h-4v-4H6v-4h4V6h4v4h4v4z" fill="currentColor"/></svg>`,
markers: [
{ id: "h1", lon: 116.4, lat: 39.9, name: "北京协和医院" },
{ id: "h2", lon: 116.42, lat: 39.88, name: "北京大学第一医院" },
{ id: "h3", lon: 116.38, lat: 39.92, name: "北京朝阳医院" },
{ id: "h4", lon: 116.44, lat: 39.93, name: "北京医院" },
{ id: "h5", lon: 116.35, lat: 39.87, name: "中日友好医院" },
],
},
{
id: "school",
name: "学校",
color: "#409eff",
shape: "school",
icon: `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9L12 3zm6.82 6L12 12.72 5.18 9 12 5.28 18.82 9zM17 15.99l-5 2.73-5-2.73v-3.72L12 15l5-2.73v3.72z" fill="currentColor"/></svg>`,
markers: [
{ id: "s1", lon: 116.32, lat: 39.95, name: "清华大学" },
{ id: "s2", lon: 116.31, lat: 39.99, name: "北京大学" },
{ id: "s3", lon: 116.45, lat: 39.92, name: "中国人民大学" },
{ id: "s4", lon: 116.28, lat: 39.87, name: "北京师范大学" },
{ id: "s5", lon: 116.48, lat: 39.85, name: "北京航空航天大学" },
{ id: "s6", lon: 116.37, lat: 39.98, name: "北京理工大学" },
],
},
{
id: "gas-station",
name: "加油站",
color: "#e6a23c",
shape: "gas-station",
icon: `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M19.77 7.23l.01-.01-3.72-3.72L15 4.56l2.11 2.11c-.94.36-1.61 1.26-1.61 2.33 0 1.38 1.12 2.5 2.5 2.5.36 0 .69-.08 1-.21v7.21c0 .55-.45 1-1 1s-1-.45-1-1V14c0-1.1-.9-2-2-2h-1V5c0-1.1-.9-2-2-2H6c-1.1 0-2 .9-2 2v16h10v-7.5h1.5v5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V9c0-.69-.28-1.32-.73-1.77zM12 10H6V5h6v5z" fill="currentColor"/></svg>`,
markers: [
{ id: "g1", lon: 116.4, lat: 39.85, name: "中石化朝阳加油站" },
{ id: "g2", lon: 116.35, lat: 39.92, name: "中石油建国路加油站" },
{ id: "g3", lon: 116.48, lat: 39.9, name: "壳牌东四加油站" },
{ id: "g4", lon: 116.28, lat: 39.88, name: "中石化望京加油站" },
{ id: "g5", lon: 116.43, lat: 39.96, name: "中石油和平里加油站" },
{ id: "g6", lon: 116.33, lat: 39.85, name: "道达尔石景山加油站" },
],
},
{
id: "shopping-mall",
name: "商场",
color: "#9c27b0",
shape: "shopping-mall",
icon: `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M18 6V4l-2-2H4v16h8v2H4c-1.1 0-2-.9-2-2V4h2v2h2V4h2v2h2V4h2v2h2V4h2v2z" fill="currentColor"/><path d="M20 8H4V6h16v2zM20 12H4v-2h16v2zM4 16h8v8H4v-8z" fill="currentColor"/></svg>`,
markers: [
{ id: "m1", lon: 116.4, lat: 39.9, name: "王府井百货" },
{ id: "m2", lon: 116.38, lat: 39.93, name: "国贸商城" },
{ id: "m3", lon: 116.45, lat: 39.87, name: "朝阳大悦城" },
{ id: "m4", lon: 116.3, lat: 39.88, name: "西单大悦城" },
{ id: "m5", lon: 116.42, lat: 39.97, name: "三里屯太古里" },
],
},
{
id: "bank",
name: "银行",
color: "#795548",
shape: "bank",
icon: `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M4 10h3v7H4v-7zm6.5 0h3v7h-3v-7zM2 19h20v3H2v-3zm15-9h3v7h-3v-7zm-5-9L2 6v2h20V6l-10-5z" fill="currentColor"/></svg>`,
markers: [
{ id: "b1", lon: 116.41, lat: 39.91, name: "中国工商银行" },
{ id: "b2", lon: 116.4, lat: 39.92, name: "中国建设银行" },
{ id: "b3", lon: 116.39, lat: 39.9, name: "中国银行" },
{ id: "b4", lon: 116.42, lat: 39.89, name: "中国农业银行" },
{ id: "b5", lon: 116.38, lat: 39.91, name: "招商银行" },
{ id: "b6", lon: 116.43, lat: 39.93, name: "交通银行" },
],
},
{
id: "park",
name: "公园",
color: "#67c23a",
shape: "park",
icon: `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M17 12h2L12 2 5.05 6.14 12 18l1-5h-4V8l2-5-1.64 3.86L12 10l4.64-3.14L15 8v5h-4l2 5z" fill="currentColor"/></svg>`,
markers: [
{ id: "p1", lon: 116.4, lat: 39.93, name: "天安门广场" },
{ id: "p2", lon: 116.33, lat: 39.95, name: "颐和园" },
{ id: "p3", lon: 116.28, lat: 39.99, name: "圆明园" },
{ id: "p4", lon: 116.47, lat: 39.92, name: "朝阳公园" },
{ id: "p5", lon: 116.35, lat: 39.86, name: "玉渊潭公园" },
{ id: "p6", lon: 116.42, lat: 39.97, name: "地坛公园" },
],
},
{
id: "hotel",
name: "酒店",
color: "#00bcd4",
shape: "hotel",
icon: `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M7 13c1.66 0 3-1.34 3-3S8.66 7 7 7s-3 1.34-3 3 1.34 3 3 3zm12-6h-8v7H3V5H1v15h2v-3h18v3h2v-9c0-2.21-1.79-4-4-4z" fill="currentColor"/></svg>`,
markers: [
{ id: "ht1", lon: 116.41, lat: 39.9, name: "北京饭店" },
{ id: "ht2", lon: 116.39, lat: 39.91, name: "王府半岛酒店" },
{ id: "ht3", lon: 116.44, lat: 39.91, name: "中国大饭店" },
{ id: "ht4", lon: 116.37, lat: 39.88, name: "希尔顿酒店" },
{ id: "ht5", lon: 116.43, lat: 39.87, name: "JW万豪酒店" },
],
},
{
id: "restaurant",
name: "餐饮",
color: "#ff7043",
shape: "restaurant",
icon: `<svg width="16" height="16" viewBox="0 0 24 24"><path d="M11 9H9V2H7v7H5V2H3v7c0 2.12 1.66 3.84 3.75 3.97V22h2.5v-9.03C11.34 12.84 13 11.12 13 9V2h-2v7zm5-3v8h2.5v8H21V2c-2.76 0-5 2.24-5 4z" fill="currentColor"/></svg>`,
markers: [
{ id: "r1", lon: 116.4, lat: 39.91, name: "全聚德烤鸭店" },
{ id: "r2", lon: 116.41, lat: 39.89, name: "东来顺火锅" },
{ id: "r3", lon: 116.38, lat: 39.92, name: "便宜坊烤鸭" },
{ id: "r4", lon: 116.42, lat: 39.88, name: "豆花餐厅" },
{ id: "r5", lon: 116.36, lat: 39.9, name: "鸿茅老字号" },
{ id: "r6", lon: 116.45, lat: 39.9, name: "新辣道鱼火锅" },
],
},
];
// 可见性控制
const visibleTypes = ref<Set<string>>(new Set(buildingTypes.map((t) => t.id)));
function toggleType(id: string) {
if (visibleTypes.value.has(id)) {
visibleTypes.value.delete(id);
} else {
visibleTypes.value.add(id);
}
visibleTypes.value = new Set(visibleTypes.value);
}
function showAll() {
visibleTypes.value = new Set(buildingTypes.map((t) => t.id));
}
function hideAll() {
visibleTypes.value = new Set();
}
function toggleAll() {
if (visibleTypes.value.size === buildingTypes.length) {
hideAll();
} else {
showAll();
}
}
function getVisibleCount(typeId: string): number {
return buildingTypes.find((t) => t.id === typeId)?.markers.length ?? 0;
}
// Popup 渲染函数
function popupRender(item: MapMarkerItem) {
const type = buildingTypes.find((t) =>
t.markers.some((m) => m.id === item.id),
);
return h("div", { class: "marker-popup" }, [
h("div", { class: "popup-header" }, [
h("span", { class: "popup-icon", style: { color: type?.color } }, [
h("span", { innerHTML: type?.icon ?? "" }),
]),
h("span", { class: "popup-title" }, item.name),
]),
h("div", { class: "popup-body" }, [
h("div", { class: "popup-row" }, [
h("span", { class: "popup-label" }, "类型:"),
h("span", { class: "popup-value" }, type?.name ?? "-"),
]),
h("div", { class: "popup-row" }, [
h("span", { class: "popup-label" }, "坐标:"),
h(
"span",
{ class: "popup-value" },
`${item.lon.toFixed(4)}, ${item.lat.toFixed(4)}`,
),
]),
]),
]);
}
</script>
<style scoped>
.building-types-demo {
display: flex;
flex-direction: column;
gap: 16px;
}
.overall-desc {
margin: 0;
font-size: 14px;
color: #606266;
line-height: 1.6;
}
/* 控制面板 */
.control-panel {
padding: 16px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
}
.control-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.control-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.control-actions {
display: flex;
gap: 8px;
}
.ctrl-btn {
padding: 4px 12px;
font-size: 12px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.ctrl-btn:hover {
background: #ecf5ff;
border-color: #409eff;
color: #409eff;
}
.type-toggles {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.type-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 13px;
border: 2px solid #e4e7ed;
background: #fff;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.type-btn:hover {
border-color: var(--type-color);
background: #f5f7fa;
}
.type-btn.active {
border-color: var(--type-color);
background: color-mix(in srgb, var(--type-color) 10%, white);
}
.type-icon {
display: flex;
align-items: center;
color: var(--type-color);
}
.type-icon :deep(svg) {
width: 16px;
height: 16px;
}
.type-name {
font-weight: 500;
color: #303133;
}
.type-count {
font-size: 12px;
color: #909399;
}
/* 图例 */
.legend {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 4px;
font-size: 12px;
}
.legend-label {
font-weight: 600;
color: #606266;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.legend-icon {
display: flex;
align-items: center;
}
.legend-icon :deep(svg) {
width: 14px;
height: 14px;
}
/* 地图容器 */
.map-wrapper {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
/* 统计面板 */
.stats-panel {
padding: 16px;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
}
.stats-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.stat-item {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f5f7fa;
border-radius: 6px;
}
.stat-icon {
display: flex;
align-items: center;
}
.stat-icon :deep(svg) {
width: 16px;
height: 16px;
}
.stat-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.stat-count {
font-size: 12px;
color: #909399;
margin-left: auto;
}
.stat-bar {
width: 100%;
height: 4px;
background: #e4e7ed;
border-radius: 2px;
overflow: hidden;
}
.stat-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s ease;
}
</style>
<style lang="scss">
/* 标记点弹窗样式 */
.marker-popup {
min-width: 160px;
font-size: 13px;
}
.popup-header {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.popup-icon {
display: flex;
align-items: center;
}
.popup-icon :deep(svg) {
width: 18px;
height: 18px;
}
.popup-title {
font-weight: 600;
font-size: 14px;
color: #303133;
}
.popup-body {
display: flex;
flex-direction: column;
gap: 4px;
}
.popup-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.popup-label {
color: #909399;
}
.popup-value {
color: #606266;
font-weight: 500;
}
</style>
样式配置
通过 markerStyle 属性配置标记点的默认样式。
形状(shape)
使用 markerStyle.shape 配置标记点的默认形状。默认为 circle(圆形)。其他形状需通过 registerMapMarkerShapes 注册。
vue
<!-- 圆形标记(默认) -->
<hx-map-markers :markers="markers" :marker-style="{ shape: 'circle', iconSize: [20, 20], color: '#f56c6c' }" />
<!-- 自定义形状(需先注册) -->
<hx-map-markers :markers="markers" :marker-style="{ shape: 'star' }" />
<!-- 自定义图标 URL -->
<hx-map-markers :markers="markers" :marker-style="{ iconUrl: 'https://example.com/icon.png', iconSize: [32, 32] }" />图标尺寸统一
圆形和图标模式使用统一的 iconSize 属性控制大小:
- 圆形模式:
iconSize[0]即为圆的直径 - 图标模式:
iconSize即为渲染尺寸
渲染优先级
标记点样式遵循以下优先级(从高到低):
1. item.iconUrl (数据项级 URL 图标,最高优先级)
2. item.render (数据项级自定义渲染函数)
3. markerStyle.render (组件级自定义渲染函数)
4. markerStyle.iconUrl (组件级图标 URL)
5. markerStyle.shape (注册的自定义形状)
6. circle (默认圆形,保底渲染方式)展示完整的 6 级渲染优先级
<template>
<ClientOnly>
<div class="priority-demo">
<h3>渲染优先级说明(从高到低)</h3>
<ol class="priority-list">
<li><code>item.iconUrl</code> — 数据项级 URL 图标(最高)</li>
<li><code>item.render</code> — 数据项级自定义渲染</li>
<li><code>markerStyle.render</code> — 组件级自定义渲染</li>
<li><code>markerStyle.iconUrl</code> — 组件级图标 URL</li>
<li><code>markerStyle.shape</code> — 注册的自定义形状</li>
<li><code>circle</code> — 默认圆形(保底)</li>
</ol>
<h4>▎场景一:item 级别优先级</h4>
<p class="desc">markerStyle 只设 shape,数据项通过 <code>iconUrl</code> / <code>render</code> 覆盖</p>
<div class="legend">
<span class="legend-item" style="color:#e74c3c">① 武汉:item.iconUrl(地标图标)</span>
<span class="legend-item" style="color:#8e44ad">② 黄石:item.render(⭐ 表情)</span>
<span class="legend-item" style="color:#409eff">⑤ 其他:shape="map-marker"</span>
</div>
<hx-map :center="{ lon: 112.5, lat: 31.0 }" :zoom="7" :height="400">
<hx-map-markers :markers="markersScene1" :marker-style="{ shape: 'map-marker', iconSize: [32, 32] }" />
</hx-map>
<h4>▎场景二:markerStyle 级别效果</h4>
<p class="desc">不同 markerStyle 配置产生的不同渲染效果</p>
<div class="legend">
<span class="legend-item" style="color:#e67e22">③ markerStyle.render(橙色圆)</span>
<span class="legend-item" style="color:#2ecc71">④ markerStyle.iconUrl(绿色定位针)</span>
<span class="legend-item" style="color:#f5a623">⑤ shape="star"(黄色星星)</span>
<span class="legend-item" style="color:#95a5a6">⑥ circle 默认(灰色圆形)</span>
</div>
<hx-map :center="{ lon: 112.5, lat: 31.0 }" :zoom="7" :height="400">
<hx-map-markers
:markers="markersScene2A"
:marker-style="{
shape: 'map-marker',
render: markerRenderFn,
iconSize: [32, 32],
}"
/>
<hx-map-markers
:markers="markersScene2B"
:marker-style="{
iconUrl: 'https://cdn-icons-png.flaticon.com/32/456/456283.png',
iconSize: [32, 32],
iconAnchor: [0.5, 0.5],
}"
/>
<hx-map-markers
:markers="markersScene2C"
:marker-style="{
shape: 'star',
iconSize: [36, 36],
}"
/>
<hx-map-markers
:markers="markersScene2D"
:marker-style="{
shape: 'circle',
iconSize: [24, 24],
color: '#95a5a6',
}"
/>
</hx-map>
</div>
</ClientOnly>
</template>
<script setup lang="ts">
import { h } from "vue"
import { HxMap, HxMapMarkers, registerMapMarkerShapes } from "@whhx/ui"
import type { MapMarkerItem, MarkerStyle } from "@whhx/ui"
// 注册自定义形状
registerMapMarkerShapes([
{
name: 'map-marker',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"/></svg>`,
defaultColor: '#409eff',
},
{
name: 'star',
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/></svg>`,
defaultColor: '#f5a623',
},
])
// === 场景一:item 级别优先级 ===
const markersScene1: MapMarkerItem[] = [
// ① item.iconUrl(最高)
{
id: 1, lon: 114.31, lat: 30.52, name: '武汉',
address: 'item.iconUrl 覆盖所有',
iconUrl: 'https://cdn-icons-png.flaticon.com/32/684/684908.png',
iconSize: [32, 32],
iconAnchor: [0.5, 0.5],
},
// ② item.render
{
id: 2, lon: 115.03, lat: 29.99, name: '黄石',
address: 'item.render 覆盖 shape',
render: () => h('div', {
style: {
fontSize: '32px', lineHeight: '1', textAlign: 'center',
width: '32px', height: '32px',
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))',
},
}, '⭐'),
},
// ⑤ shape="map-marker"(markerStyle 兜底)
{ id: 3, lon: 111.47, lat: 32.65, name: '十堰', address: '默认 shape=map-marker' },
{ id: 4, lon: 111.29, lat: 30.69, name: '宜昌', address: '默认 shape=map-marker' },
{ id: 5, lon: 112.14, lat: 32.01, name: '襄阳', address: '默认 shape=map-marker' },
]
// === 场景二:markerStyle 级别效果 ===
// ③ markerStyle.render(组件级自定义渲染)
const markerRenderFn: NonNullable<MarkerStyle['render']> = () =>
h('div', {
style: {
width: '36px', height: '36px',
background: 'linear-gradient(135deg, #e67e22, #f39c12)',
borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: '14px', fontWeight: 'bold',
boxShadow: '0 2px 6px rgba(230,126,34,0.5)',
border: '2px solid #fff',
},
}, 'R')
const markersScene2A: MapMarkerItem[] = [
{ id: 10, lon: 116.4, lat: 39.9, name: '北京', address: 'markerStyle.render 橙色圆' },
{ id: 11, lon: 121.47, lat: 31.23, name: '上海', address: 'markerStyle.render 橙色圆' },
]
// ④ markerStyle.iconUrl
const markersScene2B: MapMarkerItem[] = [
{ id: 20, lon: 113.26, lat: 23.13, name: '广州', address: 'markerStyle.iconUrl 绿色定位针' },
{ id: 21, lon: 114.06, lat: 22.54, name: '深圳', address: 'markerStyle.iconUrl 绿色定位针' },
]
// ⑤ shape="star"
const markersScene2C: MapMarkerItem[] = [
{ id: 30, lon: 104.07, lat: 30.57, name: '成都', address: 'shape=star 黄色星星' },
{ id: 31, lon: 106.55, lat: 29.56, name: '重庆', address: 'shape=star 黄色星星' },
]
// ⑥ circle 保底
const markersScene2D: MapMarkerItem[] = [
{ id: 40, lon: 108.94, lat: 34.26, name: '西安', address: 'circle 灰色圆形(保底)' },
{ id: 41, lon: 113.66, lat: 34.76, name: '郑州', address: 'circle 灰色圆形(保底)' },
]
</script>
<style scoped>
.priority-demo {
display: flex;
flex-direction: column;
gap: 16px;
}
.priority-list {
margin: 0;
padding-left: 20px;
font-size: 13px;
line-height: 1.8;
color: #606266;
}
.priority-list code {
background: #f5f7fa;
padding: 1px 6px;
border-radius: 3px;
font-size: 12px;
color: #409eff;
}
h4 {
margin: 0;
font-size: 15px;
color: #303133;
}
.desc {
margin: 0;
font-size: 13px;
color: #909399;
}
.desc code {
background: #f5f7fa;
padding: 1px 6px;
border-radius: 3px;
font-size: 12px;
color: #409eff;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 6px 12px;
background: #f5f7fa;
border-radius: 4px;
font-size: 12px;
margin-bottom: 4px;
}
.legend-item {
font-weight: 500;
}
</style>自定义形状
通过 registerMapMarkerShapes 注册自定义形状,注册后即可在 markerStyle.shape 中使用自定义名称。
typescript
import { registerMapMarkerShapes } from '@whhx/ui'
// 方式1:Raw SVG 字符串(支持颜色替换)
import starSvg from './star.svg?raw'
registerMapMarkerShapes([
{ name: 'star', svg: starSvg, defaultColor: '#f5a623' },
])
// 方式2:SVG URL(自动获取内容,支持颜色替换)
// 注意:URL 指向的 SVG 内容需要包含 currentColor 占位符
registerMapMarkerShapes([
{ name: 'custom', svg: 'https://cdn.example.com/icons/custom.svg' },
])
// 推荐方式:异步注册
await registerMapMarkerShapes([
{ name: 'marker', svg: 'https://api.iconify.design/mdi/map-marker.svg' },
])ShapeDefinition
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| name | 形状唯一标识 | string | - |
| svg | SVG 内容(raw 字符串或 URL) | string | - |
| defaultColor | 默认颜色(仅 raw SVG 模式有效) | string | '#409eff' |
| size | SVG 原始尺寸 [width, height],默认从 viewBox 或 width/height 属性解析 | [number, number] | - |
SVG 颜色替换
Raw SVG 字符串中使用 currentColor 作为颜色占位符,注册时会自动被替换为实际颜色:
svg
<!-- star.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path d="M12 2L15 9H23L17 14L19 22L12 17L5 22L7 14L1 9H9Z" fill="currentColor"/>
</svg>查询已注册形状
typescript
import { getRegisteredShapes } from '@whhx/ui'
console.log(getRegisteredShapes()) // ['star', ...]HxMapMarkers 属性
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| markers | 标记点数据 | MapMarkerItem[] | [] |
| markerStyle | 样式配置 | MarkerStyle | { shape: 'location' } |
MarkerStyle
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| shape | 形状类型(内置或自定义注册的名称) | string | 'circle' |
| iconUrl | 图标 URL(覆盖 shape) | string | - |
| iconSize | 图标渲染尺寸 [width, height](像素),同时适用于圆形和图标模式 | [number, number] | [20, 20] |
| iconOriginalSize | 图标原始尺寸,用于精确计算缩放比例 | [number, number] | - |
| iconAnchor | 图标锚点(相对比例,默认底部中心) | [number, number] | [0.5, 1] |
| render | 自定义渲染函数(返回 HTML 字符串或 VNode) | (item: MapMarkerItem) => string | VNode | - |
| color | 填充颜色(仅 shape='circle' 时生效) | string | '#ff0000' |
| radius | @deprecated 请使用 iconSize 代替 | number | - |
MapMarkerItem
| 属性 | 说明 | 类型 |
|---|---|---|
| id | 唯一标识 | string | number |
| lon | 经度 | number |
| lat | 纬度 | number |
| name | 点位名称(可选) | string |
| iconUrl | 点位图标 URL(最高优先级) | string |
| iconSize | 图标渲染尺寸 [width, height](像素) | [number, number] |
| iconOriginalSize | 图标原始尺寸 | [number, number] |
| iconAnchor | 图标锚点(比例) | [number, number] |
| render | 数据项级自定义渲染函数(优先级最高) | (item: MapMarkerItem) => string | VNode |
| extra | 扩展信息(用于自定义 popup 内容) | Record<string, string> |
| [key: string] | 其他自定义字段(可用于分组) | any |