33 · uni-app 对照查证:迁移项目怎么找"正确答案"
这是迁移期最高频的动作:写一个页面 / 接一个接口之前,先去 uni-app 老项目里确认"原来到底是怎么干的"。 本篇给的是可复制的查证路径,不是迁移方法论。配套读
32-规格驱动开发与AI协作。
开篇:概念对照
| uni-app 老项目 | Flutter 落点 | 对齐程度 |
|---|---|---|
pages/**/*.vue(template + script) | lib/ui/<feature>/views/*.dart | 结构照搬,实现重写 |
pages/api/*.js(接口定义) | lib/data/services/api/**/*_api_service.dart(Retrofit) | 路径/字段 1:1 |
Vuex store/modules/* | lib/config/providers/* + lib/ui/*/view_models/* | 不做 1:1,按 Riverpod 重设计 |
util/config.js(环境) | lib/data/services/api/environment.dart | 1:1,含路由规则 |
static/language/zh-CN.json | assets/i18n/zh-CN.json(远端覆盖、本地兜底) | key 1:1 |
| rpx / px 尺寸 | AppTokens + ScreenUtilInit(750×1624) | 不 1:1,走新 Figma 稿 |
static/general/** 位图 | assets/images/** + flutter_gen 强类型 | 路径映射,走 Assets.images.* |
| 字体图标 | AppIcon(iconfont,支持远程热更新) | 不 1:1,走 iconfont |
关键认知:老项目是业务逻辑的唯一真源,但不是实现的真源。docs/uniapp-migration-workflow.md:5 原话:
业务逻辑以 uni-app 老项目为唯一真源。
而 docs/uniapp-migration-workflow.md:13 紧接着划了另一条线:
UI 视觉与组件交互按当前 Flutter 体系实现:优先复用 He 组件,视觉值走
AppTokens,文本走RuntimeI18n,图片走 flutter_gen,图标走AppIcon。
一句话概括:"做什么"对齐老项目,"怎么做"用 Flutter 体系。
一、老项目在哪
不在本仓库。当前仓库 find . -name "*.vue" 结果为 0,老项目是独立仓库,代码里以路径引用的方式出现:
| 老项目路径(注释中的样子) | 内容 |
|---|---|
app_haier_energy_master/pages/agent/device/index.vue | 设备列表页(B/C 端混在一起) |
pages/api/device.js | 设备相关接口定义 |
util/config.js | 环境与 baseUrl 路由 |
static/language/zh-CN.json | i18n 文案真源 |
static/general/device/ | 设备类型默认位图 |
所以第一步是把老项目仓库 clone 到本地另一个目录,后面所有查证都是跨仓库比对。
二、四条查证路径(按使用频率排序)
路径 1:路由常量注释 —— 最快(80% 情况够用)
lib/routing/routes.dart 每一条路由都写了对应的 vue 文件名和参数约定:
// lib/routing/routes.dart:15-24
/// 设备加入电站入口选择页(addPlant.vue 等价)。
static const String plantSelect = '/plant-select';
/// 电站搜索列表页(selectDevice.vue 等价)。
static const String plantSelectList = '/plant-select-list';
/// 电站详情(powerDeatail,query: fatherId/systemStatus/plantStatus/plantCategory;
/// phase 电站详情)。
static const String plantDetail = '/plant-detail';
/// 电站基本信息页(baseInfo,extra: {plantId};phase 电站详情 · 概览 tab 信息卡入口)。
static const String plantBaseInfo = '/plant-base-info';一条注释给了三样东西:原 vue 名、参数类型(query / extra)、入口位置。拿到 vue 名就能直接去老项目 pages/ 下搜。
注意
powerDeatail是老项目的拼写错误(应为 Detail),迁移时原样保留在注释里。 这类"看起来是 typo 但要照抄"的地方,正是查证的价值所在。
路径 2:代码内 uni-app 注释 —— 1336 处
lib/ 下有 1336 处 uni-app 字符串、287 处 .vue 引用。Repository 层最密集:
// lib/data/repositories/plant_repository.dart:5
// 对齐 uni-app agent/home/index.vue getPlantList(GET /meta/app/plant,分页 +
// lib/data/repositories/plant_repository.dart:55
/// 电站选择列表(对齐 uni-app selectDevice.vue getPlantListNew)。
// lib/data/repositories/plant_repository.dart:64
/// 删除电站(对齐 uni-app deletePlant -> DELETE /meta/app/plant/{plantId})。常用 grep:
# 找某个功能的老实现出处
grep -rn "uni-app" lib/data/repositories/ | grep -i "device"
# 找所有引用了某个 vue 的地方
grep -rn "index.vue" lib/
# 找带 quirk 标记的刻意保留行为(最值得看)
grep -rn "quirk" lib/路径 3:openspec change 的 Why 段
openspec/changes/<name>/proposal.md 的 ## Why 第一段几乎总是写老实现在哪:
## Why
当前设备列表功能仅存在于 uniapp 旧项目
(app_haier_energy_master/pages/agent/device/index.vue),
Flutter 分中心 App 尚未实现该核心设备管理入口。
旧项目中 B 端(安装商)与 C 端(终端用户)设备页混在同一 vue 文件,
本变更只承接 B 端安装商设备列表,C 端内容拆分至独立变更 add-user-device-list。这是信息密度最高的一处:不只告诉你去哪找,还告诉你老实现有什么历史包袱(比如 B/C 端混在一个 vue)。
路径 4:spec 的 Scenario —— 查"反直觉行为"
有些行为"看起来该优化、实际不能动",只在 spec 里写明了原因:
### Requirement: Session acquisition storage order
`_acquireSession(role)` SHALL preserve the uni-app order: save selected role,
save remembered login information, call login, save accessToken, save history
account, then call getAgentInfo. It SHALL NOT call getUserInfo during immediate
login because uni-app performs that check on next app launch.(openspec/specs/auth-state/spec.md)
如果你在重构时看到一段"顺序很奇怪"的存储代码,先查 spec——大概率是刻意的。
三、迁移核对五项(docs/uniapp-migration-workflow.md:7-11)
开工前逐项核对,这是硬性要求:
| # | 核对项 | 真源 | 常见翻车点 |
|---|---|---|---|
| 1 | 文案和单位 | uni-app static/language/zh-CN.json;远端覆盖本地,本地仅兜底 | 单位(kW vs kWh)在老项目里是硬编码字符串,Flutter 侧容易漏 |
| 2 | 交互流程、API 顺序、校验、风控、加密、存储时机 | 老项目源码 1:1 | 存储先后顺序最容易错,见下面 quirk 清单 |
| 3 | 渲染结构、顺序、文字有无 | 照搬 template;冲突时保留业务行为 | 老项目"某条件下不显示某个字段",Flutter 侧常被写成始终显示 |
| 4 | 早期 rpx 过渡值 | 仅缺新 Figma 稿时按 750-baseline-adaptation-guide.md 的迁移公式 | 有新 Figma 稿就必须按 semantic token 重写,别沿用过渡值 |
| 5 | iconfont 缺失 | 用现有 iconfont 占位,位置必须有,标 TODO(icon) | 图标没找到就整个不渲染,导致布局缺一块 |
第 5 条值得单独强调:图标可以错,位置不能缺。用占位图标 + TODO(icon),比留空白更容易被发现。
四、历史包袱(quirk)清单
这些是"改了就会炸、看起来却像 bug"的地方,全部有注释或 spec 背书:
| quirk | 位置 | 为什么不能改 |
|---|---|---|
未登录时 userrole 请求头为空串 | lib/data/services/api/auth_interceptor.dart:57 | 对齐 uni-app 未登录时 USER_ROLE 存储为空串;发 null 或不发,后端行为不同 |
isRegister 的 query 字段名恒为 phone(即使账号是邮箱) | lib/data/services/api/api_service.dart:110 | 1:1 复刻 uni-app,后端就认这个字段名 |
prod 环境 baseUrl 为 null,按 serviceZone 路由 | lib/data/services/api/environment.dart:5-12 | 对齐 util/config.js:仅 betaVersion=false 时 zone.servicePath 才覆盖 baseUrl |
dev/test 固定 baseUrl,serviceZone 不起作用 | 同上 | 对齐 request2.js,不是漏实现 |
| 登录存储顺序:角色 → 记住的登录信息 → login → token → 历史账号 → getAgentInfo | openspec/specs/auth-state/spec.md | uni-app 原顺序,乱序会导致会话恢复取不到值 |
立即登录时不调 getUserInfo | 同上 | uni-app 在下次启动时才做这个检查 |
几条 quirk 的对应代码(方便直接对,都是当前 checkout 快照):
// lib/data/services/api/environment.dart:20-27 —— prod 环境 baseUrl 为 null,按 zone 路由
enum Environment {
dev('https://hemsdev.nahui-newenergy.com/api'),
test('https://hemstest.nahui-newenergy.com/api'),
prod(null); // 按 serviceZone 路由 hems*.haier-energy.com/api
const Environment(this.baseUrl);
final String? baseUrl;
}// lib/data/services/api/auth_interceptor.dart:139-141 —— userrole 头未登录时为空串
final role = await userRoleGetter?.call();
options.headers['userrole'] = role ?? '';// lib/data/services/api/api_service.dart:110-116 —— isRegister 的 query 字段名恒为 phone
@GET(ApiPaths.isRegister)
Future<ApiResponse<dynamic>> isRegister(
@Query('phone') String phone, {
@CancelRequest() CancelToken? cancelToken,
});给 AI 的提醒:这些注释里普遍带 quirk / 对齐 uni-app / TODO 标记。动手重构前 grep -rn "quirk" lib/,能省掉一次线上事故。
五、不必对齐的部分
明确边界,避免过度对齐导致 Flutter 侧写成"四不像":
| 维度 | 处理 |
|---|---|
| 视觉尺寸、间距、色值 | 不对齐老项目 rpx,走 AppTokens / 新 Figma 稿 |
| 状态管理形态 | 不照搬 Vuex module 结构,按 Riverpod + ViewModel 重设计 |
| 路由实现 | 用 GoRouter,不模拟 uni-app 的页面栈 |
| 图标实现 | 走 iconfont(AppIcon),不复制位图 |
| 组件拆分粒度 | 优先复用 he_components,不按 vue 的 template 结构硬切 |
| 老项目的 bug | 照搬——除非 change 里明确写了要修正。迁移期"行为一致"优先于"正确" |
最后一条是迁移项目的铁律:先 1:1 落地,再单独提 change 修 bug。混在一起改,出问题分不清是迁移错了还是修 bug 引入的。
六、常见坑
| 坑 | 后果 | 怎么避 |
|---|---|---|
| 只看 Figma 稿不看老项目 | 视觉对、逻辑错(少了个校验、多了个字段) | 迁移期先看 vue,再看 Figma |
| 把老项目的 typo 当错误"顺手修正" | 接口 404 / 字段名不匹配 | 照抄,确需修正单独提 change |
| 照搬 rpx 数值 | 与新 750 基线冲突,check_figma_tokens.dart 报漂移 | 有新稿走 token,无新稿走迁移公式 |
| 图标缺失就不渲染 | 布局缺块,QA 才发现 | 占位图标 + TODO(icon) |
| 重构掉"奇怪的存储顺序" | 会话恢复失败,且只在特定路径复现 | 改前先查该能力的 spec |
| 老项目 i18n key 与 Flutter 侧 key 不一致 | 线上显示 key 原文 | key 以 static/language/zh-CN.json 为准,1:1 |
| 不知道老项目要单独 clone | 查证卡住,靠猜实现 | 开工第一件事:把老项目仓库拉到本地 |
七、自检清单
- [ ] 知道老项目不在本仓库(本仓库 0 个
.vue),已单独 clone - [ ] 能按
lib/routing/routes.dart的注释,5 秒内定位任意页面对应的 vue 文件 - [ ] 会用
grep -rn "uni-app" lib/与grep -rn "quirk" lib/两条命令 - [ ] 知道迁移核对五项,并能在提 change 前逐项过一遍
- [ ] 能说出至少 3 条 quirk(userrole 空串、isRegister 的 phone 字段、prod baseUrl 为 null)
- [ ] 清楚"必须对齐"和"不必对齐"的边界:逻辑对齐、视觉走 Flutter 体系
- [ ] 知道迁移期发现老项目 bug 时,先 1:1 落地,再单独提 change 修
下一步
32-规格驱动开发与AI协作—— 查证完怎么把结论固化成 spec 与 change09-数据层链路—— 接接口时的 8 步清单,其中第 1 步就是查证老实现10-页面开发实战—— 端到端做新页面的 7 步14-国际化—— i18n key 的远端覆盖与本地兜底机制