Files
App.K12.Student.Miniapp_Flu…/.trae/skills/vue3-to-flutter/SKILL.md
T
2026-09-09 16:15:05 +08:00

235 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: "vue3-to-flutter"
description: "把 Vue3/UniApp 代码迁移成 Flutter/Dart。当用户说迁移、翻译 Vue 页面、写 Flutter 对应功能时触发。"
---
# Vue3/UniApp → Flutter 迁移助手
## 项目定位
老项目(Vue3 + UniApp)和新项目(Flutter)在同一个根目录下:
| 项目 | 路径 |
|------|------|
| Vue3 老项目 | `d:\chongqing_yuanxuan\App.K12.Student.Miniapp\App.K12.Student.Miniapp` |
| Flutter 新项目 | `d:\chongqing_yuanxuan\App.K12.Student.Miniapp_flutter` |
**迁移时,Flutter 项目是工作目录,Vue 项目是参考目录。** 读 Vue 代码做参考,写 Flutter 代码到新项目。
---
## 一、目录结构映射
Vue 端和 Flutter 端的目录对应关系必须严格遵守:
| Vue (UniApp) | Flutter | 说明 |
|---|---|---|
| `pages/tabbar/my/my.vue` | `lib/features/my/my_page.dart` | tab 页面放 features 顶层 |
| `pages/tabbar/class/components/rankItem.vue` | `lib/features/class/widgets/...dart` | 模块私有组件放 features/[模块]/widgets/ |
| `components/ConfirmBox/` | `lib/core/components/confirm_box.dart` | 全局复用组件放 core/components |
| `api/class/index.ts` + `api/class/type.ts` | `lib/core/api/class/api.dart` + `model.dart` | 接口 + 模型成对放 core/api/[模块]/ |
| `pages.json` + `tabbar/` | `lib/core/router/app_router.dart` | 路由集中在 go_router |
### 目录规则总结
1. **全局公共组件** → `lib/core/components/` (ConfirmBox、VideoPlayer、AudioPlayer 这类)
2. **模块私有组件** → `lib/features/[模块]/widgets/` (只在本模块用的子组件)
3. **业务页面** → `lib/features/[模块]/` 顶层文件(my_page.dart、class_page.dart)
4. **API 接口** → `lib/core/api/[模块]/api.dart` (单例类,如 `ClassApi.instance.getUserClassPost()`)
5. **数据模型** → `lib/core/api/[模块]/model.dart` (用 `@JsonSerializable` + `json_serializable`)
6. **全局状态** → `lib/core/providers/` (Riverpod)
7. **Isar 本地数据库** → `lib/core/storage/` (模型改了要跑 `dart run build_runner build --delete-conflicting-outputs`)
---
## 二、技术栈 / API 映射表
### 状态管理
| Vue (Pinia) | Flutter (Riverpod) | 说明 |
|---|---|---|
| `store.user.name` | `ref.watch(currentUserProvider).value?.name` | ConsumerWidget + watch 响应式读取 |
| `store.setUser()` | 直接改 provider state 或 Notifier 方法 | 写操作通过 AsyncNotifier |
| `const user = computed(...)` | `final user = ref.watch(...)` | 自动依赖追踪 |
### 网络请求
| Vue | Flutter | 说明 |
|---|---|---|
| `uni.request` / `http.get` | `DioClient.instance.get('/api/xxx')` | 统一走 DioClient 单例,自动带 Token、日志、401 处理 |
| `await api.xxx()` 返回类型 | `return XxxResponse.fromJson(data)` | 接口层返回完整模型,页面层不解析 JSON |
| `catch(e)` 在页面 | 页面层统一 try/catch,不要在 api.dart 里 catch | 异常向上抛,UI 决定怎么提示 |
### 导航与弹窗
| Vue | Flutter | 说明 |
|---|---|---|
| `uni.navigateTo({url: '/pages/xxx'})` | `context.push('/xxx')` | go_router 命名路由 |
| `uni.switchTab()` | `context.go('/')` 或直接跳转 tab 路由 | 主 tab 用 StatefulShellRoute |
| `uni.showToast({title})` | 默认 SnackBar 或项目封装的 toast | 看项目现有实现,不要自己造 |
| `uni.showModal({title, content, success})` | `await showConfirmBox(context, title:, content:, confirmText:, cancelText:)` | 全局封装在 `core/components/confirm_box.dart` |
| `<up-avatar>` / `<image>` 带远程 URL | `CachedNetworkImage(imageUrl: url)` | 自动缓存,替代 Image.network |
### 本地存储
| Vue | Flutter | 说明 |
|---|---|---|
| `uni.setStorageSync('token', xxx)` | `UserStorage.setToken(xxx)` | Token 走 flutter_secure_storage(加密) |
| `uni.getStorageSync('user')` | `UserStorage.getUser()` | 统一封装在 storage 目录 |
| 业务数据缓存 | Isar 数据库(`lib/core/storage/`) | 大批量数据用 Isar,不要放 shared_preferences |
### 图片与文件
| Vue | Flutter | 说明 |
|---|---|---|
| `uni.chooseImage()` | `PickerService.pickImages()` | 统一封装,内部处理权限、压缩、Web 兜底 |
| `uni.chooseVideo()` | `PickerService.pickVideo()` | 同上 |
| OSS 上传 | `OssUploader.uploadFile()` | 已封装,内部自动签名 |
| `stringToMd5(password)` | `md5.convert(utf8.encode(password))` | crypto 包,对齐 Vue 端 |
---
## 三、布局转换核心规则
### 像素单位
UniApp 的 `rpx` → Flutter 的 `.w / .h / .sp / .r`(通过 `flutter_screenutil`,设计稿基准 375×812)。
换算:**1rpx ≈ 0.5 设计稿 px ≈ 0.5.w**。直接把 rpx 值除以 2 加 `.w` 后缀即可。
### 布局优先级
**禁止无脑 Stack + Positioned。** 按以下优先级选择:
1. **Row / Column + Expanded + Spacer**(流式布局,首选)
2. **Flex**(更灵活的流式控制)
3. **ListView / SingleChildScrollView**(列表或可滚动内容)
4. **Stack + Positioned**(仅当确实需要层叠时,如渐变背景飘出容器)
### CSS → Flutter 对照
| CSS | Flutter | 备注 |
|---|---|---|
| `display:flex; flex-direction:column` | `Column(children: [...])` | |
| `display:flex` (水平) | `Row(children: [...])` | |
| `flex:1` | `Expanded(child: ...)` | |
| `gap: 10rpx` | `SizedBox(width/height: 5.w)` | Flutter 无 gap 属性(3.16 前),用 SizedBox 补 |
| `border-radius: 50%` | `BorderRadius.circular(999)` 或 `CircleAvatar` | |
| `box-shadow` | `BoxShadow(color:, blurRadius:, offset:)` | |
| `linear-gradient` | `LinearGradient(begin:, end:, colors:, stops:)` | |
| `overflow:hidden` | `ClipRRect` 或 `Container(clipBehavior: Clip.antiAlias)` | |
| `position: absolute; top:0; left:0` | `Positioned(top:0, left:0, child:)` | Stack 内使用 |
| `z-index` | Stack 中 children 顺序(后写的在上层) | |
| `padding: 24rpx` | `Padding(padding: EdgeInsets.all(12.w))` | 数值直接除以 2 |
| `margin: 24rpx` | `Container(margin: EdgeInsets.all(12.w))` 或 Wrap padding | |
### 高频布局模式
**固定头部 + 可滚动内容:**
```dart
// Vue: scroll-view flex:1 + 头部固定
Scaffold(
body: Column(
children: [
头部(widget),
Expanded(child: SingleChildScrollView(child: 内容)),
],
),
)
```
**卡片列表:**
```dart
ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => SizedBox(height: 16.h),
itemBuilder: (_, index) => ItemCard(data: items[index]),
)
```
**渐变背景飘出(Vue HeaderBac 模式):**
```dart
Stack(
clipBehavior: Clip.none, // 允许溢出
children: [
Positioned(top:0, left:0, right:0, child: 背景图/渐变),
SafeArea(child: Column(children: [内容])), // 决定 Stack 高度
],
)
```
---
## 四、约束清单(必须遵守)
### 绝对禁止
1. **不要重复造轮子**:检查 `core/` 目录下是否已有封装(DioClient、showConfirmBox、PickerService、OssUploader、CachedNetworkImage、talker),能用就直接用。
2. **不要把私有组件放顶层**:只在一个业务模块用的组件放 `features/[模块]/widgets/`,别丢到 `core/components/`。
3. **不要裸写 Dio 或 http**:所有请求必须走 `DioClient.instance.get/post`。
4. **不要裸用 showDialog 写确认弹窗**:用 `showConfirmBox`。
5. **不要手写 JSON 解析**:API 返回用 `@JsonSerializable` + `fromJson`,不要手动 Map 转对象。
6. **不要在 build 方法里做网络请求**:请求放 provider 的 AsyncNotifier 或页面 initState。
7. **不要在 api.dart 里 catch 异常**:异常向上抛,UI 层决定怎么处理。
8. **不要新造 Toast/Loading/Empty 组件**:检查 core/components 是否已有。
### 必须遵守
1. **页面用 ConsumerWidget**(Stateless + ref.watch),有本地交互状态用 ConsumerStatefulWidget。
2. **尺寸必须加 `.w/.h/.sp/.r`**(flutter_screenutil),设计稿 375×812。
3. **加了适配后缀的 widget 不能用 `const`**(值是运行时计算的)。
4. **新图片资源必须在 pubspec.yaml 的 flutter.assets 里声明**(assets 不递归)。
5. **Isar 模型改字段后必须跑 `dart run build_runner build --delete-conflicting-outputs`**。
6. **JSON 模型改字段后必须跑 `dart run build_runner build`**(生成 *.g.dart)。
7. **路由跳转用 go_router**:`context.push('/xxx')` / `context.go('/')`,不要 Navigator.push。
8. **网络日志开关**:`DioClient.enableNetworkLog = false`(默认关,调试临时开)。
---
## 五、迁移步骤
每次迁移一个页面/组件,按这个流程走:
1. **读 Vue 源码**:先完整读完 Vue 页面的 template + script + style,理解业务逻辑。
2. **确认依赖**:
- Vue 端用了哪些 `api/` 接口?→ Flutter 端 `core/api/[模块]/api.dart` 里有没有?没有就补。
- Vue 端用了哪些全局 `components/`?→ Flutter 端 `core/components/` 有没有对应封装?没有就补。
- Vue 端用了 `pinia store` 哪些字段?→ Flutter 端 `core/providers/` 里的 Riverpod provider 有没有?
3. **搭 Flutter 骨架**:按 Vue 端的布局结构,用 Row/Column/Expanded 搭出 widget 树。
4. **填数据绑定**:Riverpod ref.watch / AsyncValue 处理 loading / error / data 三态。
5. **写交互逻辑**:onTap / async 方法,调 api.dart,更新 provider state。
6. **处理样式**:rpx → .w,CSS → Flutter 布局。
7. **处理弹窗/Toast**:用项目已有封装。
8. **自测验证**:热重载看效果,对比 Vue 端视觉。
---
## 六、高频坑清单
| 坑 | 现象 | 解决 |
|---|---|---|
| rpx 直接当数值写 | 元素偏小/偏大 | 必须除以 2 加 `.w` 后缀 |
| Stack + Positioned 导致点击区域丢失 | 按钮点不动 | Positioned 没给 width/height 时不参与 hitTest;改用流式布局 |
| SingleChildScrollView 嵌套无界 ListView | 布局报错 | 要么用 CustomScrollView + Sliver,要么 ListView 加 shrinkWrap |
| Image.network 头像列表卡顿 | 滚动掉帧 | 换 CachedNetworkImage + errorBuilder |
| assets/ 不递归 | 新图片加载失败 | pubspec.yaml 的 flutter.assets 里手动加子目录 |
| Isar 模型改字段忘记 build_runner | Schema 报错 | 改完模型立刻跑 build_runner |
| build 方法里发请求 | 每次重建都触发 | 放 initState 或 ref.listenManual |
| DioError 不存在 | 编译报错 | Dio 5.x 改名为 DioException |
| TalkerDioLogger 没关 | 控制台刷屏 | DioClient.enableNetworkLog = false |
| Stack 内 Positioned 没绑 bottom | 布局错乱 | 需要背景图飘出容器时,上层内容决定 Stack 高度,背景图 Positioned(top:0,left:0,right:0) |
| 渐变背景导致角落透明 | Stack 加 BorderRadius 后背景没跟着裁 | 用 ClipRRect 或 Container(clipBehavior: Clip.antiAlias) 包整个 Stack |
---
## 七、Vue 端需要注意的坑(迁移时保留)
Vue 端存在的容错逻辑,迁移 Flutter 时不要丢掉:
1. **网络状态判断**:Vue 端可能有 `uni.getNetworkType()` / 断网提示 → Flutter 用 `connectivity_plus` + `internet_connection_checker_plus`(项目已有 `network_status_provider.dart`)。
2. **离线缓存**:Vue 端可能有请求失败时读本地缓存的逻辑 → Flutter 用 Isar 缓存或 shared_preferences。
3. **提交前校验**:表单提交前的必填校验、网络判断 → Flutter TextEditingController + onSubmitted 时校验。
4. **平台条件编译**:`#ifdef APP-PLUS` / `#ifdef H5` → Flutter 用 `Platform.isIOS / Platform.isAndroid` 或直接不考虑(项目只做 App)。
5. **节流防抖**:Vue 端搜索、分页加载的 throttle/debounce → Flutter 用 `Timer` 或 debounce 包。
6. **分页游标**:Vue 端用 `pageIndex + prevPageId` → Flutter `PagingController<int, T>`(项目已有 `common_paged_list.dart` 封装)。