第 14 章 实战:构建一个门店地图应用
从前 13 章走到这里,你已经掌握了 mapcn 的全部核心能力。本章把它们串成一个完整 可运行的「门店地图」:左侧是可筛选的门店列表,右侧是地图,二者通过选中状态双向 联动;地图用聚合呈现全部门店,点击单点弹出详情;并带搜索与主题切换。
14.1 数据结构
ts
interface Store {
id: string;
name: string;
category: string; // 餐饮 / 零售 / 服务
lng: number;
lat: number;
address: string;
rating: number;
}把若干门店组织成 GeoJSON FeatureCollection,便于交给 MapClusterLayer:
ts
const storesToFC = (stores: Store[]): GeoJSON.FeatureCollection => ({
type: "FeatureCollection",
features: stores.map((s) => ({
type: "Feature",
properties: { id: s.id, name: s.name, category: s.category, rating: s.rating },
geometry: { type: "Point", coordinates: [s.lng, s.lat] },
})),
});14.2 主组件骨架
tsx
"use client";
import { useMemo, useState } from "react";
import { Map, MapClusterLayer, MapPopup, MapControls } from "@/components/ui/map";
import { Card } from "@/components/ui/card";
export function StoreMap({ stores }: { stores: Store[] }) {
const [selected, setSelected] = useState<Store | null>(null);
const [query, setQuery] = useState("");
const filtered = useMemo(
() => stores.filter((s) => s.name.includes(query) || s.category.includes(query)),
[stores, query],
);
const fc = useMemo(() => storesToFC(filtered), [filtered]);
return (
<div className="grid h-[600px] grid-cols-[320px_1fr] gap-4">
{/* 左侧列表 */}
<Card className="overflow-auto p-3">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索门店…"
className="mb-3 w-full rounded border px-2 py-1 text-sm"
/>
{filtered.map((s) => (
<button
key={s.id}
onClick={() => setSelected(s)}
className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-accent"
>
<div className="font-medium">{s.name}</div>
<div className="text-muted-foreground text-xs">{s.category} · {s.rating}★</div>
</button>
))}
</Card>
{/* 右侧地图 */}
<Card className="relative overflow-hidden p-0">
<Map center={[116.4, 39.9]} zoom={10}>
<MapClusterLayer<Store>
data={fc}
onPointClick={(f) => {
const p = f.properties;
setSelected(stores.find((s) => s.id === p.id) ?? null);
}}
/>
{selected && (
<MapPopup
longitude={selected.lng}
latitude={selected.lat}
onClose={() => setSelected(null)}
closeButton
closeOnClick={false}
>
<div className="space-y-1">
<p className="font-semibold">{selected.name}</p>
<p className="text-muted-foreground text-xs">{selected.address}</p>
<p className="text-xs">{selected.category} · {selected.rating}★</p>
</div>
</MapPopup>
)}
<MapControls />
</Map>
</Card>
</div>
);
}14.3 联动逻辑解析
- 列表 → 地图:点击列表项
setSelected(s),地图中对应的MapPopup在selected坐标处弹出; - 地图 → 列表:点击聚合中的单点触发
onPointClick,用要素id反查stores得到完整对象并setSelected(...),列表可用selected高亮当前项(此处略去高亮样式); - 搜索:
query过滤filtered,useMemo派生fc,聚合层随之更新——这正符合 第 13 章「大数据用useMemo缓存」的建议。
14.4 还能怎么扩展
这个骨架已经覆盖了真实项目最常见的模式,接下来可自然延伸:
- 分类筛选:用
paint表达式为不同category着色(见第 8 章); - 路由规划:选中两个门店后,调用 OSRM 画出驾车路线(见第 7 章);
- 详情弹窗富化:把
MapPopup内容换成带图片、评分、按钮的卡片(见第 6 章); - 性能:门店过万时,
MapClusterLayer的聚合天然扛得住,无需改动结构。
14.5 收尾:你已经学到了什么
回顾全书:
- 第 1–2 章:理解 mapcn 的定位并完成安装;
- 第 3–4 章:渲染第一张地图、加上控件;
- 第 5–6 章:标记、弹窗、提示框;
- 第 7–10 章:路线、弧线、GeoJSON、聚合;
- 第 11–12 章:深入实例、主题与自定义底图;
- 第 13 章:性能与最佳实践;
- 第 14 章:用一个完整案例落地。
你现在已经具备用 mapcn 构建生产级地图应用的能力。
本章小结
- 门店地图 =
MapClusterLayer(聚合)+MapPopup(详情)+ 列表(侧栏)的联动; - 列表与地图通过共享
selected状态双向联动; - 搜索用
useMemo派生 GeoJSON,聚合层自动更新; - 骨架可直接扩展为分类着色、路线规划、富弹窗等;
- 至此,你已掌握从入门到进阶的全部核心能力。
愿你的地图,既好看,又好用。