Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Address、Area 与地图能力

地址模块看上去很普通,但在 oak-general-business 里,它实际上把三类东西放到了一起:

  • 业务地址 Address
  • 行政区划 Area
  • 地图与地铁辅助能力

因此,这一章不只是“收货地址怎么存”,还包括地址选择器、区域树以及地图服务的注入方式。

主要对象

这一章涉及的实体有:

  • Address
  • Area
  • Subway
  • Station
  • SubwayStation

其中:

  • Address 是业务地址;
  • Area 是只读的行政区划字典;
  • Subway / Station / SubwayStation 则提供地铁线路与站点关系。

近期 Area 数据已经进入全球 locale 化。项目如果展示多语言地址或区域选择器,应把区域名称当作 i18n 数据的一部分同步,而不是在页面里手写地区名映射。

组件

围绕这些对象,已经有一组很实用的组件:

  • src/components/address/list
  • src/components/address/upsert
  • src/components/area/upsert
  • src/components/pickers/area
  • src/components/config/upsert/map
  • src/components/subwayLine/list
  • src/components/subwayLine/picker
  • src/components/subwayLine/upsertStation
  • src/components/subwayLine/upsertSubway
  • src/components/amap/map
  • src/components/amap/location

这说明地址模块并不局限于“表单里填几项字段”,而是包含了一整套选择和辅助展示能力。

address/listaddress/upsert

公共包里的这两个组件是最基础的一组地址页能力。

address/list 当前行为很直接:

  • 读取 namephonedetailarea
  • 自动把 省市区 + detail 组合成展示文本
  • 点击某一项时跳到 /address/upsert?oakId=...
  • 没有数据时展示“创建”按钮并跳到 /address/upsert

也就是说,公共实现更像“最小地址簿”,默认没有:

  • 默认地址切换
  • 删除按钮
  • 当前用户过滤
  • 业务对象级别的定制文案

address/upsert 则负责最基础的地址编辑,当前会直接维护:

  • name
  • phone
  • areaId
  • detail

它内部还有两个很关键的默认行为:

  • confirm() 会直接 execute() 然后 navigateBack()
  • callAreaPicker() 默认跳 /pickers/area

所以项目层如果路由沿用公共约定,接起来会非常顺;如果路由不是 /pickers/area,就需要像业务项目那样包一层自己的页面壳。

pickers/area 常用参数

src/components/pickers/area/index.ts 的关键参数很少,但非常重要:

  • depth,默认是 3
  • onAreaSelected

它的真实行为是:

  • 初始只查“国家”下一层区域
  • 如果点到的区域 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

这是一个数组,每项主要维护:

  • key
  • type

其中 type 当前支持:

  • personal
  • special
  • business

源码里还有两个很值得写进文档的行为:

  • 如果配置多个地图服务,系统默认优先使用第一个
  • 高德地图这边允许配置多个 key,组件文案也明确提示可轮换使用

而在启动注入处,优先级是:

  • 先用 MapWorld
  • 没有 MapWorld 再用 AMap

所以项目里不要同时瞎配两套然后期待运行时自动做复杂路由,公共实现不是这么设计的。

amap/mapamap/location

如果你的项目要直接复用地图 React 组件,这两组能力最好拆开理解。

amap/map 更偏底层地图容器,常用参数包括:

  • akey
  • version
  • mapProps
  • mapRef
  • useAMapUI
  • uiVersion
  • uiCallback
  • securityJsCode
  • serviceHost

它适合:

  • 只展示地图
  • 在地图上挂自己的 marker / overlay / 自定义控件
  • 自己控制 MapProps

amap/location 则是更完整的“选点对话框”,常用参数包括:

  • akey
  • visible
  • onClose
  • onConfirm
  • geolocationProps
  • useGeolocation
  • dialogProps
  • securityJsCode
  • serviceHost

它已经把:

  • 地图拖拽选点
  • POI 搜索
  • 当前定位
  • 结果确认

这些交互做完整了。所以项目里如果只是要“让用户选一个地址点位”,优先用 amap/location,不要自己再重拼一套地图搜索弹窗。

subwayLine/* 组件更适合什么场景

这组组件更偏城市服务、线路筛选或门店覆盖范围,不是收货地址的必选项。

subwayLine/list 当前会:

  • 读取 subway -> subwayStation$subway -> station
  • 组装成树结构
  • 默认带 areaId='330100' 的过滤
  • ready() 时加载所有城市级 area 选项

subwayLine/picker 的关键参数有:

  • areaId
  • onCancel
  • onConfirm(stationIds)
  • selectIds

它适合做“按地铁站多选筛选”的场景。

而:

  • subwayLine/upsertStation 主要参数是 openStationonClosesubwayId
  • subwayLine/upsertSubway 主要参数是 openSubwayonClose

它们更偏后台字典维护,不是前台用户常用组件。

aspect / endpoint / feature

地址本身没有专门的 frontend feature、aspect 或 endpoint。

这并不代表它是“弱能力”,只是意味着:

  • 地址对象的读写主要通过普通 Oak 组件完成;
  • 地图能力不是通过本仓库里的自定义 aspect 暴露,而是通过 oak-common-aspect 和启动注入点来接入;
  • 区域树相关的查询辅助则放在 src/utils/area.ts,已经提供了 makeAreaAncestorFilter(...)makeAreaDecendantFilter(...) 两个 helper。
  • 区域多语言数据需要跟随 make:locale 和数据升级流程进入运行时。

另外有一个很容易忽略的真实依赖关系:

  • oak-pay-businessOrderShip 实体都直接引用了 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/listcomponents/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/upsert
  • src/components/pickers/area
  • src/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/mapamap/location,就要先确认 System.config.Map 已经配好,否则前端组件就算能渲染,后端注入的地图服务能力也不完整。

如果你的项目需要复杂的地址定位、路线或地图服务,先看这一套注入链路,再决定要不要在项目层扩展。