2026-10-20-campus-map-design.md 12 KB

全国校区地图 · 设计文档

  • 日期:2026-10-20
  • 分支:dev
  • 状态:待评审(本阶段只出设计,不写实现)

1. 背景与目标

已有「店铺运营」按学校分组看数据,但缺少一个全国视角:哪些学校在用、分布在哪、开了几家。

新增一级菜单 校区地图:

  • 中国地图上展示所有学校;启用的学校点亮(涟漪 + 名称),停用的显示为灰点
  • 可在界面上创建学校:填名称、在地图上点出位置、启用
  • 点击点位或列表项可查看/编辑/停用/删除

2. 已定的关键决策

决策 结论 原因
位置来源 必须在地图上主动点选,不提供经纬度输入框 一个城市有多所高校,城市中心会重叠且偏差大;手填经纬度对使用者不友好
默认位置 没有默认位置,未落点不允许保存 避免"看起来创建成功、点却落在市中心"
城市字段 只作为地图跳转的辅助(选城市 → 地图飞过去 → 再点精确位置),不决定落点 兼顾"快速定位到城市"和"精确到校区"
省份 落点后自动识别(点在哪个省),也可手动改 避免坐标与省份对不上
坐标系 统一存 GCJ-02(底图坐标系) 点选天然得到 GCJ-02;混用 WGS84 会有几百米偏差(见 §4.3)

3. 交互设计

3.1 创建学校(主流程)

[新建学校] → 弹窗:学校名称
                   可选:省份 → 城市(仅用于把地图飞过去)
                   [取消] [在地图上选择位置]
        ↓
   弹窗收起,地图进入「定位模式」:
     顶部提示条:请在地图上点击学校所在位置 · Esc 取消
     鼠标变十字;可选省份时先飞到该省/市(zoom 6~9)
        ↓
   点击地图 → 落一个临时标记(脉冲动画)
     提示条变成:已选位置(自动识别的省份) [重选] [确认创建]
        ↓
   确认 → 保存并默认「启用」→ 点位变橙色涟漪,列表新增一条

要点:

  • 没点位置时"确认创建"不可用,从流程上杜绝城市中心兜底
  • 落点后自动识别省份;识别不出(点在海上/境外)时提示"该位置不在中国境内,请重新选择"
  • 同城多校因为各自精确点选,天然分散,不会叠在一起

3.2 编辑与重新定位

列表项或点位 → 右侧详情卡:

┌ 中国计量大学 ────────────────┐
│ 浙江省 · 杭州市               │
│ 坐标 120.3626, 30.3187        │  ← 只读展示,不可编辑输入框
│ [重新定位]  [停用]  [删除]     │
└──────────────────────────────┘
  • 重新定位:再次进入定位模式,地图飞到该点(zoom 9),点击新位置后确认覆盖
  • 名称可改(点详情卡标题进入编辑)
  • 删除需二次确认

3.3 启用 / 停用

  • 列表每项一个开关;打开 = 点亮,关闭 = 灰点
  • 开关即时生效,不需要保存按钮
  • 停用只影响地图展示,不删除学校、不影响「店铺运营」数据

3.4 地图交互

操作 行为
滚轮 / 双指 缩放(zoom 3 ~ 12)
拖拽 平移
双击省份 放大到该省
悬停点位 tooltip:学校名 · 省/市 · 状态
点击点位 选中并高亮,右侧滚动到对应卡片
搜索框 按名称过滤列表;点结果 → 地图飞到该点(zoom 9)
[回到全国] 恢复默认视野(center 104, 35 / zoom 1.2)

3.5 空态与异常

场景 展示
一所学校都没有 地图灰显 + 中央「新建第一所学校」按钮 + 一行说明
存储不可用 地图正常显示,顶部提示「学校数据暂不可用」+ 重试,创建按钮禁用
底图加载失败 提示「地图数据加载失败,请刷新」,不渲染空白画布
名称重复 保存时提示「已存在同名学校」,不创建
点位在境外 拒绝保存并提示重新选择

4. 地图技术方案

4.1 技术选型

  • ECharts 5.6.0(仓库已内置 public/vendor/echarts.min.js),geo 组件 + effectScatter 系列
  • 底图:中国省级 GeoJSON,本地内置 public/vendor/china-map.json(569KB,35 个省级面,含南海诸岛)
  • 城市定位库:public/vendor/cn-cities.json(19KB,35 省 / 477 个城市区县的中心坐标,只用于"飞到城市")
  • 全部离线,不依赖任何地图服务或 Key

4.2 点亮实现

geo(底图,roam 可缩放平移)
├── effectScatter  series「已启用」
│     symbolSize 10~14,颜色 #ff6b35
│     rippleEffect { brushType: 'stroke', scale: 3.2, period: 3.5 }
│     label { show: true, formatter: 学校名, position: right }
│
└── scatter        series「未启用」
      symbolSize 6,颜色 #b8b2ad,无涟漪、无标签
  • 点击:chart.on('click', params => selectSchool(params.data.id))
  • 悬停:tooltip formatter 输出三行(名称 / 省市 / 状态)
  • 选中:被选中的点 symbolSize +4、边框加亮,右侧卡片同步高亮
  • 同城多点重叠:先按经纬度绘制;若两点屏幕像素距离 < 14px,对后者做 ≤8px 的确定性偏移(按 id 哈希),保证都看得见

4.3 坐标系(重要)

中国互联网地图(阿里 DataV、高德)用的是 GCJ-02,而 GPS / OpenStreetMap 是 WGS84,两者在国内相差 300~600 米。

例子 WGS84 GCJ-02 偏差
中国计量大学 120.3580, 30.3210 120.3626, 30.3187 约 440m / 254m
杭州市中心 120.1536, 30.2875 120.1583, 30.2851 约 452m / 257m
天安门 116.3910, 39.9070 116.3972, 39.9084 约 533m / 155m

结论:

  • 用户在底图上点选的坐标天然是 GCJ-02,直接存,所见即所得
  • 外部来源(GPS、OSM、别人给的经纬度)是 WGS84,入库前要转换;提供 wgs84ToGcj02() 工具函数与一条 scripts/ 小工具供批量导入
  • 只读展示坐标时标注「GCJ-02」,避免误用

5. 数据模型

复用现有 ops 存储(默认 SQLite,可切 PG),新增一张表:

CREATE TABLE IF NOT EXISTS ops_school (
  id         text PRIMARY KEY,          -- 服务端生成,如 sch_20261020_a1b2c3
  name       text NOT NULL,
  province   text NOT NULL DEFAULT '',
  city       text NOT NULL DEFAULT '',
  lng        real NOT NULL,             -- GCJ-02
  lat        real NOT NULL,
  enabled    integer NOT NULL DEFAULT 1,
  created_at text NOT NULL,
  updated_at text NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS ops_school_name_idx ON ops_school (name);

设计说明:

  • name 唯一,防止重复创建同一所学校
  • province / city 为展示与分组用;province 由落点自动识别,city 取用户在下拉里选的值(允许为空)
  • 与「店铺运营」的关联:ops_shop.school 是字符串,按 名称相等 关联;创建时会提示「与 N 个运营数据中的学校匹配」,对不上也允许创建(先画图、后补数据)
  • enabled 用 0/1,SQLite 与 PG 都兼容

6. 接口

方法 路径 说明
GET /api/schools 列表(含 enabled、坐标、省市)
POST /api/schools 创建;{ name, province, city, lng, lat, enabled? }
POST /api/schools/:id 更新(改名、重新定位)
POST /api/schools/:id/toggle 启用/停用 { enabled }
DELETE /api/schools/:id 删除

校验:

  • name:去空白、1~60 字符、唯一
  • lng / lat:必填;中国范围(lng 73~136、lat 3~54)之外拒绝
  • province / city:可选,≤40 字符
  • 存储不可用:GET 返回 200 + { enabled:false, reason },写操作 503(与 ops 现有一致)

7. 界面布局

┌──────────────────────────────────────────────────────────────┐
│ 校区地图                         + 新建学校                  │
│ 已启用 3 / 共 5                                              │
├───────────────────────────────────┬──────────────────────────┤
│                                   │ 🔍 搜索学校               │
│         中国地图(可缩放平移)      │ ┌──────────────────────┐ │
│                                   │ │● 中国计量大学 [开]    │ │
│        ● 中国计量大学              │ │  浙江 · 杭州          │ │
│        ● 浙江工商大学桐庐校区       │ ├──────────────────────┤ │
│        ○ 浙江科技学院(未启用)     │ │○ 浙江科技学院 [关]    │ │
│                                   │ │  浙江 · 杭州          │ │
│                  [回到全国]        │ └──────────────────────┘ │
└───────────────────────────────────┴──────────────────────────┘
  • 左侧地图占 2/3 以上,右侧列表 320px
  • 列表按省份分组,组头显示该省启用数(如「浙江省 2/3」)
  • 窄屏(<1100px)时列表折叠到地图下方

8. 与现有功能的关系

  • 独立菜单,不改「店铺运营」的现有行为
  • 数据上通过 ops_shop.school 名称关联,为下一步「点击地图上的学校 → 打开店铺运营并筛选该学校」留好接口(本期不做跳转)
  • 不参与采集、不参与钉钉推送、不影响任何现有接口

9. 测试

9.1 单元

  • 坐标范围校验(境内/境外)
  • 名称去重与去空白
  • 省份自动识别(点在浙江/北京/境外三种情况)
  • WGS84 → GCJ-02 转换(用 §4.3 三组已知值做断言,误差 < 1e-4)

9.2 接口(临时 SQLite)

  • 创建 → 列表可见;重复名称 400
  • 切换启用状态;删除后列表消失
  • 坐标缺省 / 越界 → 400
  • 存储不可用 → GET 200 + enabled:false,POST 503

9.3 冒烟

GET /api/schools 200(只读,不新增写操作)

9.4 手工验收

新建「中国计量大学」→ 选浙江省 → 地图飞到浙江 → 点下沙位置 → 保存 → 点位点亮并显示名称 → 刷新页面仍在 → 停用变灰 → 重新启用再点亮 → 删除后消失。

10. 待办清单(通过评审后拆实施计划)

  • public/vendor/china-map.json、cn-cities.json(已就绪,需登记到静态白名单)
  • ops_school 表与存储方法(SQLite + PG 两驱动)
  • /api/schools 五个端点 + 校验 + 存储降级
  • public/schools.js:地图渲染、点亮、定位模式、列表联动、创建/编辑弹窗
  • index.html 菜单与页面骨架、app.js 视图注册、style.css
  • 坐标工具:wgs84ToGcj02() + 省份识别(点在多边形内)
  • 测试脚本扩展(单元 + 接口)与 smoke 只读断言
  • README 增补「校区地图」章节与坐标系说明

11. 开放问题(实施前需确认)

  1. 学校列表是否要显示「该学校在店铺运营里有几家店 / 最近有数据的日期」?(需要与 ops 数据联查,工作量 +0.5 天)
  2. 删除学校时,如果「店铺运营」里已有同名学校的数据,是否需要二次确认并提示影响?
  3. 是否需要「批量导入学校」(给一份 Excel/CSV,含名称+坐标)?本期默认不做。