Address、Area 与地图能力
地址模块看上去很普通,但在 oak-general-business 里,它实际上把三类东西放到了一起:
- 业务地址
Address - 行政区划
Area - 地图与地铁辅助能力
因此,这一章不只是“收货地址怎么存”,还包括地址选择器、区域树以及地图服务的注入方式。
主要对象
这一章涉及的实体有:
AddressAreaSubwayStationSubwayStation
其中:
Address是业务地址;Area是只读的行政区划字典;Subway/Station/SubwayStation则提供地铁线路与站点关系。
近期 Area 数据已经进入全球 locale 化。项目如果展示多语言地址或区域选择器,应把区域名称当作 i18n 数据的一部分同步,而不是在页面里手写地区名映射。
组件
围绕这些对象,已经有一组很实用的组件:
src/components/address/listsrc/components/address/upsertsrc/components/area/upsertsrc/components/pickers/areasrc/components/config/upsert/mapsrc/components/subwayLine/listsrc/components/subwayLine/pickersrc/components/subwayLine/upsertStationsrc/components/subwayLine/upsertSubwaysrc/components/amap/mapsrc/components/amap/location
这说明地址模块并不局限于“表单里填几项字段”,而是包含了一整套选择和辅助展示能力。
address/list 与 address/upsert
公共包里的这两个组件是最基础的一组地址页能力。
address/list 当前行为很直接:
- 读取
name、phone、detail、area - 自动把
省市区 + detail组合成展示文本 - 点击某一项时跳到
/address/upsert?oakId=... - 没有数据时展示“创建”按钮并跳到
/address/upsert
也就是说,公共实现更像“最小地址簿”,默认没有:
- 默认地址切换
- 删除按钮
- 当前用户过滤
- 业务对象级别的定制文案
address/upsert 则负责最基础的地址编辑,当前会直接维护:
namephoneareaIddetail
它内部还有两个很关键的默认行为:
confirm()会直接execute()然后navigateBack()callAreaPicker()默认跳/pickers/area
所以项目层如果路由沿用公共约定,接起来会非常顺;如果路由不是 /pickers/area,就需要像业务项目那样包一层自己的页面壳。
pickers/area 常用参数
src/components/pickers/area/index.ts 的关键参数很少,但非常重要:
depth,默认是3onAreaSelected
它的真实行为是:
- 初始只查“国家”下一层区域
- 如果点到的区域
depth !== props.depth,继续按parentId往下钻 - 只有点到目标深度,才触发
onAreaSelected(id)
这意味着:
- 收货地址、门店地址这类一般都应该保持
depth=3 - 如果你只想选到城市,可以把
depth改成2
taicang 的真实接法也很典型:
- 单独给它包了
/frontend/pickers/area页面 - 在地址编辑组件里再把它包成一个
visible/onClose/onChange的弹层选择器
也就是说,公共组件负责区域树查询,项目层负责“放成独立页还是弹窗”。
config/upsert/map 常用字段
地图配置页本质上是在编辑 System.config.Map。当前主要支持两套配置:
1. mapWorld
字段很少,核心就是:
webApiKey
2. amaps
这是一个数组,每项主要维护:
keytype
其中 type 当前支持:
personalspecialbusiness
源码里还有两个很值得写进文档的行为:
- 如果配置多个地图服务,系统默认优先使用第一个
- 高德地图这边允许配置多个 key,组件文案也明确提示可轮换使用
而在启动注入处,优先级是:
- 先用
MapWorld - 没有
MapWorld再用AMap
所以项目里不要同时瞎配两套然后期待运行时自动做复杂路由,公共实现不是这么设计的。
amap/map 与 amap/location
如果你的项目要直接复用地图 React 组件,这两组能力最好拆开理解。
amap/map 更偏底层地图容器,常用参数包括:
akeyversionmapPropsmapRefuseAMapUIuiVersionuiCallbacksecurityJsCodeserviceHost
它适合:
- 只展示地图
- 在地图上挂自己的 marker / overlay / 自定义控件
- 自己控制
MapProps
amap/location 则是更完整的“选点对话框”,常用参数包括:
akeyvisibleonCloseonConfirmgeolocationPropsuseGeolocationdialogPropssecurityJsCodeserviceHost
它已经把:
- 地图拖拽选点
- POI 搜索
- 当前定位
- 结果确认
这些交互做完整了。所以项目里如果只是要“让用户选一个地址点位”,优先用 amap/location,不要自己再重拼一套地图搜索弹窗。
subwayLine/* 组件更适合什么场景
这组组件更偏城市服务、线路筛选或门店覆盖范围,不是收货地址的必选项。
subwayLine/list 当前会:
- 读取
subway -> subwayStation$subway -> station - 组装成树结构
- 默认带
areaId='330100'的过滤 - 在
ready()时加载所有城市级area选项
subwayLine/picker 的关键参数有:
areaIdonCancelonConfirm(stationIds)selectIds
它适合做“按地铁站多选筛选”的场景。
而:
subwayLine/upsertStation主要参数是openStation、onClose、subwayIdsubwayLine/upsertSubway主要参数是openSubway、onClose
它们更偏后台字典维护,不是前台用户常用组件。
aspect / endpoint / feature
地址本身没有专门的 frontend feature、aspect 或 endpoint。
这并不代表它是“弱能力”,只是意味着:
- 地址对象的读写主要通过普通 Oak 组件完成;
- 地图能力不是通过本仓库里的自定义 aspect 暴露,而是通过
oak-common-aspect和启动注入点来接入; - 区域树相关的查询辅助则放在
src/utils/area.ts,已经提供了makeAreaAncestorFilter(...)和makeAreaDecendantFilter(...)两个 helper。 - 区域多语言数据需要跟随
make:locale和数据升级流程进入运行时。
另外有一个很容易忽略的真实依赖关系:
oak-pay-business的Order、Ship实体都直接引用了oak-general-business里的Address
所以地址这章并不只是“用户中心的小功能”,支付、物流域本身就在依赖它。
后台规则
地址域自己的 checker 很简单:
src/checkers/address.ts会校验手机号是否合法。
而 src/triggers/address.ts 当前并没有额外触发器逻辑。这反而说明这个模块的后端规则比较干净,更多复杂性留在前端选择与地图辅助上。
注入点
地图能力的真正注入点,不在地址组件里,而在 oak-general-business/src/routines/start.ts。
启动时它会:
- 向
oak-common-aspect注册getMapService; - 按
application.system.config.Map的配置,优先实例化MapWorld,否则再实例化AMap。
所以如果你看到 components/amap/* 能正常工作,根本原因不是组件自己神奇,而是后台启动时已经把地图服务注入进去了。
项目中如何接入
地址和地图能力在项目里的接法,重点不在页面,而在系统配置和启动注入:
- 先在
System.config.Map里配置地图服务; - 如果你要做后台配置页,直接复用
src/components/config/upsert/map; - 启动时由
routines/start.ts注册getMapService; - 页面里再复用地址、区域、地图组件;
- 如果你要自己拼区域查询,优先复用
src/utils/area.ts里的树过滤 helper。
因此这章的项目接入通常是“先把地图配置好,再去放组件”。
真实项目里的接法
taicang 已经给出了一套很典型的项目拆法:
/frontend/address/list页面负责地址簿入口/frontend/address/upsert页面负责地址新增/编辑/frontend/pickers/area页面单独承接区域选择- 项目组件
components/address/myAddress/list、components/address/myAddress/upsert再包一层业务行为
这一层项目包装主要补了三件事:
- 地址只看当前登录用户
- 支持默认地址切换
- 移动端交互改成弹层区域选择和底部保存按钮
也就是说,公共包提供的是“地址能力底座”,真实项目通常还会再补一层“我的地址”“发货地址”这类业务语义组件。
项目层常见的补充规则
公共包默认不会强制“同一对象只能有一个默认地址”,但 taicang/src/triggers/address.ts 已经示范了最常见的项目规则:
- 当某条地址的
default=true时,把同一entity/entityId下其它默认地址全部取消
如果你的项目也需要默认地址,一般都应该在项目层补这条 trigger,而不是指望前端自己保证唯一性。
使用示例
1. 在系统配置里打开地图服务
await this.features.config.updateConfig('system', systemId, {
Map: {
amaps: [{ key: 'your-amap-key', type: 'personal' }],
},
});
2. 在表单页直接复用地址与区域组件
更推荐的页面组合通常是:
src/components/address/upsertsrc/components/pickers/areasrc/components/amap/location
这样区划选择、地址编辑、定位和地图展示能直接沿用公共包现成的交互,不需要项目层再写一遍。
2.1 taicang 里更真实的包法
taicang 当前不是直接把公共 address/upsert 原封不动地塞进页面,而是:
- 在项目组件里把
AreaPicker包成visible/onClose/onChange - 创建态自动把
entity='user'、entityId=currentUserId补进去 - 同时补了
default这一层业务语义
这是一种非常推荐的接法。公共组件负责通用字段,项目组件负责“这个地址属于谁、默认地址怎么切、移动端怎么交互”。
3. 在项目查询里复用区域树 helper
如果你的业务对象不是直接挂 address 组件,而是自己做筛选列表,更推荐直接复用公共 helper:
import { makeAreaDecendantFilter } from '@oak-general-business/utils/area';
const districtFilter = makeAreaDecendantFilter(
{ id: '330100' },
2,
true
);
这个 helper 适合拿来做“某个城市下所有区县”“某个行政区下所有下级区域”这类 Oak filter 组合。
4. 选点场景优先复用 amap/location
如果你的项目需要的是“用户在地图上选门店位置、活动位置、服务地址”,更推荐直接复用:
<Location
akey={amapKey}
visible={visible}
onClose={() => setVisible(false)}
onConfirm={(poi) => {
update({
longitude: poi.location.lng,
latitude: poi.location.lat,
address: poi.address,
});
}}
/>
这样拖图、搜索、定位、确认结果都已经在公共组件里处理好了。
使用建议
对新手来说,这一章最值得记住的是:
Address只负责“业务上要保存的地址”;Area负责“可选的标准区划”;- 地图能力是启动时注入的,不要在组件里自己重复初始化一套地图 SDK。
再补三条实战里非常容易踩坑的点:
- 地址类表单通常应该把
areaId落到区县级,否则公共组件里拼接parent.parent.name时很容易出现展示不完整。 - 如果项目里有“默认地址”,最好在项目 trigger 里保证唯一,而不是只靠前端互斥。
- 只要页面要用
amap/map或amap/location,就要先确认System.config.Map已经配好,否则前端组件就算能渲染,后端注入的地图服务能力也不完整。
如果你的项目需要复杂的地址定位、路线或地图服务,先看这一套注入链路,再决定要不要在项目层扩展。