Files
2026-09-09 16:15:05 +08:00

12 KiB
Raw Permalink Blame History

name, description
name description
vue3-to-flutter 把 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

高频布局模式

固定头部 + 可滚动内容:

// Vue: scroll-view flex:1 + 头部固定
Scaffold(
  body: Column(
    children: [
      头部(widget),
      Expanded(child: SingleChildScrollView(child: 内容)),
    ],
  ),
)

卡片列表:

ListView.separated(
  itemCount: items.length,
  separatorBuilder: (_, __) => SizedBox(height: 16.h),
  itemBuilder: (_, index) => ItemCard(data: items[index]),
)

渐变背景飘出(Vue HeaderBac 模式):

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 封装)。