一 . 在开发项目的时候,需要在地图上绘制带颜色,文字,角标的小水滴标记类似地图气泡针

实现方式有两种:
- SVG: 矢量图生成图标,性能好,缩放不清晰
- HTML+CSS : 通过DOM+样式生成marker
二. 实现方式对比
两种实现方式各有优劣,可根据项目点位数量,清晰度要求,开发周期选择,具体对比如下:
|----------|---------------------------------------|-------------------------------------------|---------------------------------------------|
| 实现方式 | 优点 | 缺点 | 适用场景 |
| HTML+CSS | 开发快,DOM操作灵活,颜色/文字修改便捷,无需掌握SVG语法,调试成本低 | 矢量性差,缩放易模糊,性能略逊于SVG(大量点位时),适配高分屏需额外处理 | 点位数量少(≤100个)、对缩放清晰度要求不高、开发周期短的场景 |
| SVG | 矢量特性优,缩放无模糊,性能好(支持大量点位),优化后适配所有高分屏 | 需处理高分屏模糊问题,SVG语法略复杂,调试图标细节(如角标,三角)需精准计算坐标 | 点位数量多(>100个)、对缩放清晰度要求高、需长期维护的场景(如车辆监控、点位管理) |
二. 实现方式
1.HTML+CSS
javascript
//车辆点标记
// 用 HTML+CSS 生成一个气泡 DOM
function createPinDOM({
modelLabel = "",
coulombColor = "#2ac66d",
textColor,
minTag = "",
badgeColor = "#b277c8",
badgeTextColor = "#fff"
}) {
const el = document.createElement("div");
el.className = "pin";
// 动态颜色(用 CSS 变量)
el.style.setProperty("--pin-color", coulombColor);
el.style.setProperty("--text-color", textColor || coulombColor);
el.style.setProperty("--badge-color", badgeColor);
el.style.setProperty("--badge-text", badgeTextColor);
el.innerHTML = `
<div class="pin__inner">
<div class="pin__text">${modelLabel ?? ""}</div>
</div>
${minTag ? `<div class="pin__badge">${minTag}</div>` : ""}
`;
return el;
}
export class VehicleLabels {
constructor(map) {
this.map = map;
this.items = []; // { marker, data } 列表
this.index = new Map(); // vehicleId -> marker
}
clear() {
this.items.forEach(({ marker }) => marker.setMap(null));
this.items = [];
this.index.clear();
}
draw(list = [], { fit = false } = {}) {
this.clear();
const bounds = new AMap.Bounds();
list.forEach(item => {
// 位置
const pos = Array.isArray(item.latestAxis)
? item.latestAxis
: JSON.parse(item.latestAxis);
const content = createPinDOM({
modelLabel: item.modelLabel,
coulombColor: item.coulombColor,
textColor: item.textColor || item.coulombColor,
minTag: item.minTag,
badgeColor: item.badgeColor,
badgeTextColor: item.badgeTextColor
});
const marker = new AMap.Marker({
position: pos,
anchor: "bottom-center", // 底部尖点对准坐标
content,
offset: new AMap.Pixel(0, 0),
clickable: true,
zIndex: 200
});
marker.setExtData(item);
marker.setMap(this.map);
this.items.push({ marker, data: item });
if (item.vehicleId != null) this.index.set(item.vehicleId, marker);
bounds.extend(pos);
});
if (fit && this.items.length) {
this.map.setFitView(null, false, [30, 30, 30, 30], 18);
}
}
// 更新:支持按 id 或过滤函数
update(idOrFilter, patch = {}) {
const targets =
typeof idOrFilter === "function"
? this.items.filter(({ marker }) => idOrFilter(marker.getExtData()))
: [this.index.get(idOrFilter)]
.filter(Boolean)
.map(m => ({ marker: m }));
targets.forEach(({ marker }) => {
const ext = Object.assign({}, marker.getExtData(), patch);
marker.setExtData(ext);
// 位置
if (patch.latestAxis) {
const pos = Array.isArray(patch.latestAxis)
? patch.latestAxis
: JSON.parse(patch.latestAxis);
marker.setPosition(pos);
}
// DOM 内容(颜色/文字/角标)
const el = marker.getContent();
if (!el) return;
if ("coulombColor" in patch)
el.style.setProperty("--pin-color", patch.coulombColor);
if ("textColor" in patch || "coulombColor" in patch)
el.style.setProperty(
"--text-color",
patch.textColor ||
patch.coulombColor ||
ext.textColor ||
ext.coulombColor
);
if ("modelLabel" in patch) {
const t = el.querySelector(".pin__text");
if (t) t.textContent = patch.modelLabel ?? "";
}
if ("minTag" in patch) {
let b = el.querySelector(".pin__badge");
if (patch.minTag) {
if (!b) {
b = document.createElement("div");
b.className = "pin__badge";
el.appendChild(b);
}
b.textContent = patch.minTag;
} else if (b) {
b.remove();
}
}
if ("badgeColor" in patch)
el.style.setProperty("--badge-color", patch.badgeColor);
if ("badgeTextColor" in patch)
el.style.setProperty("--badge-text", patch.badgeTextColor);
});
}
}
样式,我放在了公共scss文件
html
/* DOM 标记:水滴主体(绿色圆 + 底部三角) */
.pin {
position: relative;
width: 40px; // 直径
height: 40px;
border-radius: 50%;
background: var(--pin-color, #2ac66d); // 外圈颜色:动态
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
// transform: translate(-50%, -100%); // 锚点移到底部中心
will-change: transform;
/* 底部小三角(尖头) */
&::after {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -10px;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 12px solid var(--pin-color, #2ac66d); // 同外圈颜色
}
/* 内部白色圆盘 */
&__inner {
position: absolute;
inset: 5px;
border-radius: 50%;
background: #fff;
display: grid;
place-items: center;
}
/* 中间型号文字 */
&__text {
font:
700 14px/1 system-ui,
-apple-system,
"Segoe UI",
Arial;
color: var(--text-color, #16a34a); // 文字颜色:动态
letter-spacing: 0.2px;
}
/* 右下角角标(小圆/可当胶囊) */
&__badge {
position: absolute;
z-index: 999;
right: -2px;
bottom: -2px;
min-width: 18px;
height: 18px;
padding: 0 4px;
border-radius: 999px; // 圆/胶囊
background: var(--badge-color, #b277c8); // 角标底色:动态
color: var(--badge-text, #fff); // 角标文字色:动态
font:
700 12px/18px system-ui,
Arial;
text-align: center;
box-shadow: 0 0 0 2px #fff; // 白描边提清晰度
pointer-events: none;
}
}
实现效果

使用方法
//绘制车辆点标记 一个参数是数组,另一个参数是配置对象
this.vehicleLayer.draw(res.findAllVO.vehicleAxisList, {
fit: false, // 绘制点位完成后是否调整视野到点标记区域
showLabel: true, // 显示车辆 ID
})
2.svg方式
javascript
export class VehicleLabels {
/**
* @param {AMap.Map} map 地图实例
* @param {Object} config 配置项
* @param {Function|Object} [config.colorMap] 颜色映射:函数(item)=>'#hex' 或 {ok:'#..',warn:'#..'}
* @param {Number} [config.size=36] 圆标尺寸
*/
constructor(map, config = {}) {
console.log("配置项", config);
this.map = map;
//合并配置项
this.cfg = Object.assign({ size: 36, colorMap: null }, config);
//创建一个LabelsLayer 图层,管理点标记
this.layer = new AMap.LabelsLayer({
zooms: [3, 20],
zIndex: 200,
collision: false
// animation: false
});
this.map.add(this.layer);
//存储所有点位
this.items = []; // AMap.LabelMarker 实例列表
//创建一个索引表(查找字典), 后面用Id直接找到某个点的Marker实例
this.index = new Map();
}
// -------------------- public APIs --------------------
//清空方法
clear() {
this.layer.clear();
this.items = [];
this.index.clear();
}
/**
* 批量绘制
* item 结构示例:
* { vehicleId:11223, latestAxis:'[113.32,23.13]', code:'F18', status:'ok', bottomText:'停/沉/...', color:'#2ecc71' }
*/
draw(list = [], opts = {}) {
console.log("绘制车辆点方法list", list);
console.log("opts配置对象", opts);
//绘制完成后是否自动调整视野
const { fit = false } = opts;
//清空旧的点
this.clear();
//生成新的marker 把每辆车的数据编程一个LabelMarker对象
const markers = list.map(item => this._makeMarker(item));
this.layer.add(markers);
this.items = markers;
markers.forEach(m => this.index.set(m.getExtData().vehicleId, m));
if (fit && markers.length) this.map.setFitView(markers, false, null, 18);
}
/**
* 动态更新单个或多个点位
* @param {Number|Function} idOrFilter vehicleId 或 (ext)=>boolean
* @param {Object} patch 可更新字段:latestAxis, code, status, color, bottomText
*/
update(idOrFilter, patch = {}) {
const targets =
typeof idOrFilter === "function"
? this.items.filter(m => idOrFilter(m.getExtData()))
: [this.index.get(idOrFilter)].filter(Boolean);
targets.forEach(m => {
const ext = Object.assign({}, m.getExtData(), patch);
// 位置
if (patch.latestAxis) {
const pos = Array.isArray(patch.latestAxis)
? patch.latestAxis
: JSON.parse(patch.latestAxis);
m.setPosition(pos);
}
// 图标(颜色/中心文字/徽标)
if (
"color" in patch ||
"status" in patch ||
"code" in patch ||
"badge" in patch
) {
m.setIcon(this._buildIcon(ext));
}
// 底部文字
if ("bottomText" in patch) {
if (patch.bottomText) {
m.setText(this._buildBottomText(patch.bottomText));
} else {
m.setText(null);
}
}
m.setExtData(ext);
});
}
// 无论缩放都显示
setAlwaysVisible() {
this.layer.setOptions?.({ zooms: [3, 20], collision: false });
this.items.forEach(m => m.setOptions?.({ zooms: [3, 20] }));
}
// -------------------- internals --------------------
//把一条数据转换成地图上的一个点对象
_makeMarker(item) {
console.log("转换item", item);
const pos = Array.isArray(item.latestAxis)
? item.latestAxis
: JSON.parse(item.latestAxis);
//创建一个点标记实力
const marker = new AMap.LabelMarker({
//点的位置
position: pos,
//这个点在哪些缩放级别下显示
zooms: [3, 20],
icon: this._buildIcon(item),
text: item.bottomText
? this._buildBottomText(item.bottomText)
: undefined,
extData: item
});
return marker;
}
_buildBottomText(content) {
return {
content: String(content),
direction: "bottom",
offset: [0, -6],
style: {
fontSize: 12,
fillColor: "#333",
backgroundColor: "rgba(255,255,255,.95)",
padding: [2, 6],
borderRadius: 6,
strokeColor: "#e5e7eb",
strokeWidth: 1
}
};
}
_resolveColor(item) {
// 优先 item.color;其次根据 status 用 colorMap;否则默认蓝
if (item.color) return item.color;
const { colorMap } = this.cfg;
if (typeof colorMap === "function") return colorMap(item) || "#1791fc";
if (colorMap && item.status && colorMap[item.status])
return colorMap[item.status];
return "#1791fc";
}
//生成点的样式
_buildIcon(item) {
const size = 50;
const dpr = Math.max(2, Math.round(window.devicePixelRatio || 1));
const W = size * dpr;
const H = size * dpr;
const cx = W / 2;
const cy = H / 2 - 4 * dpr; // 上移,给三角留位置
const R = (size / 2 - 4) * dpr;
// 动态参数
const fillMain = item.coulombColor || "#2ac66d"; // 外圈颜色
const textColor = item.coulombColor || "#16a34a"; // 中间文字颜色
const text = item.modelLabel ?? ""; // 型号文字
const badge = item.minTag ?? ""; // 角标文字
const badgeFill = item.badgeColor || "#b277c8"; // 角标底色
const badgeTextC = item.badgeTextColor || "#fff"; // 角标文字色
// 角标位置
const badgeR = size * 0.18 * dpr;
const bx = cx + R * 0.5;
const by = cy + R * 0.5;
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">
<!-- 水滴外圈 -->
<circle cx="${cx}" cy="${cy}" r="${R}" fill="${fillMain}" />
<!-- 下方三角形 -->
<path d="M ${cx - 8 * dpr} ${cy + R - 2 * dpr}
L ${cx + 8 * dpr} ${cy + R - 2 * dpr}
L ${cx} ${cy + R + 10 * dpr} Z"
fill="${fillMain}" />
<!-- 内圆白底 -->
<circle cx="${cx}" cy="${cy}" r="${R - 5 * dpr}" fill="#fff" />
<!-- 中间型号文字 -->
<text x="${cx}" y="${cy}" text-anchor="middle"
font-family="system-ui,Arial"
font-size="${10 * dpr}" font-weight="700"
fill="${textColor}" dominant-baseline="middle">${text}</text>
${
badge
? `
<!-- 右下角标 -->
<circle cx="${bx}" cy="${by}" r="${badgeR}" fill="${badgeFill}" stroke="#fff" stroke-width="${2 *
dpr}" />
<text x="${bx}" y="${by}" text-anchor="middle"
font-size="${10 * dpr}" font-weight="700"
fill="${badgeTextC}" dominant-baseline="middle">${badge}</text>`
: ""
}
</svg>
`.trim();
return {
type: "image",
size: [size, size],
anchor: "bottom-center", // 关键!水滴底部对准坐标点
image: "data:image/svg+xml;utf8," + encodeURIComponent(svg)
};
}
}
实现效果
size=50

size=36

总体来说我感觉有点模糊
优化方案(解决高分屏模糊问题)
核心思路: 按设备像素比(dpr) 放大SVG画布绘制, icon,size仍使用目标显示尺寸,相当于"2x/3x高清图",在高分屏上显示更锐利,同时优化细节提升可读性
优化后 _buildIcon 方法(核心优化代码)
javascript
_buildIcon(item) {
const size = this.cfg.size || 36;
const tipH = Math.round((size/2 - 2) * 0.55);
const dpr = Math.max(2, Math.round(window.devicePixelRatio || 1)); // 放大倍率,最低2倍,适配高分屏
const W = size * dpr;
const H = (size + tipH) * dpr;
// 尺寸都按 dpr 放大,确保绘制精度
const cx = Math.floor((size/2) * dpr);
const cy = cx;
const R = Math.floor((size/2 - 2) * dpr);
const rInner = Math.max(10*dpr, R - 5*dpr);
const fillMain = item.color || '#2ac66d';
const strokeMain = 'rgba(0,0,0,.15)';
const textColor = item.textColor || '#16a34a';
const text = item.code ?? '';
const badge = item.badge ?? '';
const badgeFill = item.badgeColor || '#b277c8';
const badgeTextC = item.badgeTextColor || '#fff';
const tipHpx = Math.round((R) * 0.55);
const tipWpx = Math.round((R) * 0.55);
const tipPath = [
`M ${cx - tipWpx/2} ${cy + R*0.55}`,
`L ${cx} ${cy + R + tipHpx}`,
`L ${cx + tipWpx/2} ${cy + R*0.55}`,
'Z'
].join(' ');
// 角标位置精准计算,提升细节
const badgeR = Math.round(size * 0.17 * dpr);
const bx = cx + Math.round(R * 0.45);
const by = cy + Math.round(R * 0.35);
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">
<!-- 去掉滤镜,避免浑浊;打开几何渲染,提升清晰度 -->
<g shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
<circle cx="${cx}" cy="${cy}" r="${R}" fill="${fillMain}" stroke="${strokeMain}" stroke-width="${2*dpr}"/>
<path d="${tipPath}" fill="${fillMain}" stroke="${strokeMain}" stroke-width="${1.5*dpr}"/>
<circle cx="${cx}" cy="${cy}" r="${rInner}" fill="#fff"/>
<text x="${cx}" y="${cy + 1*dpr}" text-anchor="middle"
font-family="system-ui,Arial"
font-size="${Math.max(12*dpr, Math.floor(rInner*0.8))}"
font-weight="700" fill="${textColor}"
dominant-baseline="middle"
paint-order="stroke fill" stroke="#fff" stroke-width="${0.5*dpr}">
${text}
</text>
${badge ? `
<circle cx="${bx}" cy="${by}" r="${badgeR}" fill="${badgeFill}" stroke="#ffffff" stroke-width="${2*dpr}"/>
<text x="${bx}" y="${by + Math.max(0, Math.floor(badgeR*0.15))}" text-anchor="middle"
font-size="${Math.max(10*dpr, Math.floor(badgeR*0.95))}" font-weight="700"
fill="${badgeTextC}" dominant-baseline="middle">${badge}</text>
` : ''}
</g>
</svg>
`.trim();
return {
type: 'image',
// ⚠️ 展示尺寸用"目标尺寸",不要乘dpr,避免显示异常
size: [size, size + tipH],
anchor: 'bottom-center',
image: 'data:image/svg+xml;utf8,' + encodeURIComponent(svg)
};
}
优化要点:
- SVG画布宽高 x dpr; 颞部所有坐标/尺寸/字体也 x dpr: 核心是"高清绘制,正常显示",避免高分屏(如手机,Retina屏)下的模糊问题,相当于绘制2x/3x高清图,再压缩到目标尺寸显示,即保证清晰度,又不改变标记在地图上的实际大小
- 返回的size: [w,h] 不要乘dpr: size 是标记在地图上的"显示尺寸", 乘dpr会导致标记被放大,出现显示异常(如水滴过大,遮挡地图内容),保持原始尺寸才能贴合预期效果
- 去掉滤镜(阴影)和过重的半透明描边: 原SVG代码可能存在滤镜导致的图表浑浊问题,去掉冗余滤镜,简化描边,既能提升清晰度,又能减少SVG绘制成本,避免页面卡顿
- paint-order="stroke fill" + 极细白描边: 让文字边缘更清晰,避免文字背景融合(如浅色文字配浅色背景),及细白描边即能提升文字辨识度,又不会显得突兀,兼顾美观和可读性
效果
size=50

size=36
