WX_TO_UNIAPP_RULES.md 24 KB

微信小程序转 UniApp 详细迁移指南

⚠️ 核心规则提示 本项目的核心强制规则(如接口规范、工具类替代、样式单位等)已提取至根目录下的 .cursorrules 文件。 在进行代码转换时,AI 助手必须优先遵循 .cursorrules 中的指令。 本文档作为详细参考手册,提供具体的代码示例、复杂场景说明和完整的迁移背景。

核心避坑指南 (详细版)

以下规则是 .cursorrules 的补充说明,包含具体代码示例:

  1. 接口导入:严禁使用 config/api 或相对路径引用接口。必须使用 @/pagesPersonal/service/... (分包) 或 @/service/... (主包)。
    • 重要:接口导入必须使用解构导入(Named Imports),严禁使用默认对象导入(import service from ...)。
    • 示例:import { updateAccountAuthList as updateAccountAuthListApi, backstageDelUserMember_V3 } from '@/pagesPersonal/service/patientManagement';
  2. 接口返回必须遵循 7.8 接口封装调用规范。业务层调用 Service 方法时,必须使用解构获取返回值(如 let { resp, resData } = await api())。严禁只解构 { resp } 或假设接口直接返回数据对象。
  3. 参数解码onLoad 解析参数时,JSON.parse 必须包裹在 try-catch 中,防止因字符截断导致的白屏。
  4. Dataset:模板中保留 data-xxx 时,JS 中必须使用 e.currentTarget?.dataset?.xxx 安全访问,防止 undefined 报错。
  5. 变量冲突:接口方法名与本地变量冲突时,必须重命名本地变量(如 acceptCredit -> targetStatus)。
  6. 样式单位:严禁使用 rpx必须全文批量替换为 upx
  7. 工具类替代
    • publicFn.getMember -> await useGetMember()
    • util.getAuthorize -> common.getAuthorize
    • utils 导入 -> import { common } from '@/utils' (Common) / import icon from '@/utils/icon' (Icon)
    • Legacy Utils: getState.jspagesPatientFn.js 必须替换为 TS 版本:
      • 引用路径:import { getState, pagesPatientFn } from '@/uni-app-base/utils'
      • 物理文件:uni-app-base/utils/getState.tsuni-app-base/utils/pagesPatientFn.ts
    • 注意icon 是一个静态资源路径,必须使用 ref(icon) 包裹,防止在模板中直接引用导致的路径错误。变量名为 iconUrlimage 标签上有相关的 src 都要用 iconUrl 进行获取。 21→ 22→8. 代码完整性
    • 严禁漏掉代码:迁移时必须逐行核对小程序源码(js/wxml/wxss),确保所有逻辑、事件绑定、样式规则都已迁移。
    • 平台差异处理:如遇到无法直接迁移的逻辑(如 xbossTrack),严禁直接删除,必须在对应位置保留 TODO 注释并说明原因。

1. 项目结构与配置

配置文件

  • app.json
    • 内容迁移至 pages.json
    • pages 数组保持不变,路径无需修改。
    • subpackages 分包配置直接迁移到 pages.jsonsubPackages 字段。
    • window 配置迁移至 pages.jsonglobalStyle
    • tabBar 配置迁移至 pages.jsontabBar
  • 入口文件
    • app.js 的逻辑迁移至 App.vue
    • globalData 建议迁移至 VuexPinia 状态管理,或者直接挂载到 Vue.prototype / app.config.globalProperties
    • onLaunch, onShow, onHide 生命周期对应迁移到 App.vue
    • 当前项目使用 App.vue + 自定义 hook 完成启动逻辑(如版本检查、应用状态判断、登录、配置拉取、WebSocket 链接等),迁移时请将原 app.js 流程拆分为 Hook 并在 App.vue 中顺序调用。
    • 全局数据 globalData:当前项目依旧使用 getApp().globalData 存储运行期上下文数据(如 accountInfohosIdchannelIdhasWebsocket 等),迁移时优先复用此约定;如需长期状态请同步到 store。
  • 路由配置
    • 若有生成新文件,需要在 pages.json 添加路由(pagessubPackages)以确保可访问。
    • 路由检测:执行转换时,需检测当前文件是否有路由。若该文件是非组件(不在 components 文件夹下)且没有路由,必须在 pages.json 中定义。

2. 页面与组件文件转换

文件后缀

  • .wxml -> .vue (放在 <template> 标签内)。
  • .wxss -> .vue (放在 <style> 标签内)。
  • .js -> .vue (放在 <script> 标签内)。
  • .json -> 删除。
    • 配置项(如 navigationBarTitleText)迁移至 pages.jsonstyle 字段。
    • 标题提取:若当前同级存在 .json 文件,navigationBarTitleText 必须从该 .json 文件中获取并同步到 pages.json
    • 组件引用 (usingComponents) 迁移至 pages.json (全局) 或 .vuecomponents 选项 (局部)。
  • 独立文件转换规则(必须修改后缀,禁止保留原小程序后缀):
    • .wxss (公共样式) -> .scss
    • .js (工具/逻辑) -> .ts
    • 注意:修改文件名的同时,必须同步更新代码中的引用路径(如 @import '...common.wxss' -> @import '...common.scss')。
  • 文件清理:页面/组件转换并验证通过后,必须删除同级目录下的原微信小程序文件(.wxml, .wxss, .js, .json),只保留 .vue 文件(及必要的独立 .scss/.ts 工具文件)。

JS 逻辑转换

  • Page({}) 替换为 export default {}
  • data: { ... } 替换为 data() { return { ... } }
  • this.setData({ key: value }) 替换为 key.value = value
    • 注意:对于路径更新 this.setData({ 'a.b': 1 }),需改为 a.value.b = 1
  • properties (组件) 替换为 props
    • 推荐写法:使用 TypeScript 泛型语法配合 withDefaults,确保类型安全与默认值。 ts const props = withDefaults(defineProps<{ userInfo?: any; type?: string; }>(), { userInfo: () => ({}), type: 'default' });
  • 组件方法暴露:原小程序通过 this.selectComponent 调用的组件方法,在 <script setup>必须使用 defineExpose({ methodName }) 显式暴露,否则父组件无法调用。
  • 页面上下文获取:组件内若需获取当前页面路由或参数,可继续使用 getCurrentPages() 获取页面栈(注意判空与类型转换)。
  • 生命周期映射

    • onLoad -> onLoad (UniApp 保留页面生命周期)。
    • onShow -> onShow
    • created, attached, detached (组件) -> created, mounted, beforeDestroy (Vue 生命周期)。
    • 当前项目页面常用模式(推荐按此迁移):

      <script lang="ts" setup>
      import { ref } from 'vue';
      import { useOnLoad } from '@/hook';
      // 使用 getApp() 读取运行时全局数据
      const app = getApp();
      const list = ref<any[]>([]);
      useOnLoad(async (options) => {
      // 迁移原 onLoad 逻辑至此,保留原字段处理
      // 复杂逻辑请加简要中文注释
              
      //  参数解析容错处理
      if (options && options.myInfo) {
        try {
          const info = JSON.parse(decodeURIComponent(options.myInfo));
          // 业务逻辑...
        } catch (e) {
          console.error('参数解析失败', e);
          // 必要的兜底逻辑
        }
      }
      });
      </script>
      

      模板语法转换

      • wx:if, wx:elif, wx:else -> v-if, v-else-if, v-else
      • wx:for="{{list}}" -> v-for="(item, index) in list" :key="index"
      • wx:key="id" -> :key="item.id"
      • bindtap="handler" -> @click="handler"
      • catchtap="handler" -> @click.stop="handler"
      • bindinput -> @input
      • data-xxx="{{value}}" -> 建议改为函数传参 @click="handler(value)"。如果必须保留,使用 :data-xxx="value",在事件对象 e.currentTarget.dataset.xxx 中获取。
      • 安全访问:在 JS 中获取 dataset 时,必须使用可选链操作符:const val = e.currentTarget?.dataset?.xxx;,避免因事件冒泡或元素结构变化导致 currentTarget 为空引发的报错。
      • 注意 v-forv-if 同时出现在同一元素会导致作用域问题(Vue 3 中 v-if 优先级更高),务必使用 <template v-for>...</template> 包裹并在内部元素上写 v-if,如: vue <template v-for="(item,index) in list" :key="index"> <view v-if="item?.IsShow == '1'">{{ item.MenuName }}</view> </template>
  • 列表截取展示:若需限制列表渲染数量(如仅展示前3条),禁止 v-forv-if 混用(如 v-if="index < 3"),推荐在 v-for 中直接使用 .slice(0, n) 对数组进行切片,如 v-for="(item, index) in list.slice(0, 3)"

  • 深层属性访问需判空:对于 menuObj.Children[0].Children 等层级,需在模板中加入 v-if="menuObj?.Children?.[0]?.Children" 保护。

3. API 转换

命名空间

  • 绝大多数 wx. API 可直接替换为 uni. (如 wx.request -> uni.request)。

特殊 API

  • wx.getStorageSync -> uni.getStorageSync
  • wx.createSelectorQuery -> uni.createSelectorQuery().in(this) (注意必须加 .in(this) 以确保在组件内正确查找)。
  • wx.navigateToMiniProgram -> uni.navigateToMiniProgram(当前项目中个别地方仍保留 wx 调用,迁移时统一改成 uni)。
  • 设备信息获取兼容:低版本微信基础库可能缺少 uni.getDeviceInfo,需容错回退 uni.getSystemInfoSync(),避免字符串方法调用报错(已在 uni_modules/mp-html/components/mp-html/parser.js 中处理)。

网络请求

  • 项目中的 utils/request.js 需要适配 uni.request,注意 header 和返回值的差异(uni-app 返回数组 [error, res] 或 Promise)。
  • 当前项目通过 vite.config.js 配置了代理与环境变量,H5 开发走本地代理 /api -> VITE_APP_PROXY_API_BASE_URL。小程序端请直接使用后端绝对域名,或在运行时使用 useDomain() 获取域名拼接接口路径。
  • 链接与域名拼接:富文本解析(mp-html)内已实现 domain 拼接与 url 处理(参考 pagesAdmin/article/detail/detail.vue 中的 :domain="domain")。

4. 样式与资源

  • 单位:项目内统一采用 upx(已对部分页面做了从 rpxupx 的更正)。迁移时请保持一致。
  • 背景图片<style> 中的背景图引用建议使用绝对路径(/static/...)或网络路径,避免相对路径带来的构建问题。
  • 全局样式app.wxss 内容迁移至 App.vue<style> 或单独的 common.css 并在 App.vue 中引入。
  • 样式保持:严禁随意添加非必要的 CSS 样式。必须严格保留原小程序的样式逻辑,仅在出现明显布局错乱或单位转换问题时进行最小化调整。

5. 第三方库适配 (xbossTrack 重点)

当前问题

xbossTrack 通过劫持 PageApp 构造器实现埋点,这在 UniApp (基于 Vue) 中会失效。

适配方案

  1. 废弃 wrapper.js:不再使用 Page = ... 这种方式。
  2. 使用 Global Mixin:在 main.js 中使用 Vue.mixin 全局混入生命周期逻辑。

    // 示例思路
    Vue.mixin({
      onShow() {
         // 调用 timeTracker 逻辑
      },
      methods: {
         // 劫持 methods 需要在 beforeCreate 中遍历 this.$options.methods 进行包装
         // 或者利用拦截器思路
      }
    })
    
    1. Element Tracker
    2. 原有逻辑依赖 catchtap 冒泡捕获。在 Vue 中,建议创建一个自定义指令 v-track 或者在全局点击事件中处理(但要注意 Vue 事件处理机制)。
    3. 最简单的迁移方式是保留 elementTracker 方法,并在需要的元素上显式绑定 @click="elementTracker($event)",或者重构为自动监听页面点击。

    6. 其他注意事项

    • 分包预加载:检查 preloadRule 配置。
    • 自定义 TabBar:如果使用了自定义 TabBar,需要按照 UniApp 的自定义 TabBar 规范重写。
    • Towxml:Markdown 渲染库建议替换为 UniApp 插件市场中兼容 Vue 的 Parser 插件 (如 mp-html)。

    7. 当前项目约定与依赖用法(迁移需遵循)

    7.1 Hook 与页面生命周期

    • 使用自定义 hook 管理页面加载与启动逻辑:
    • useOnLoad((options)=>{ ... }):迁移原 onLoad 代码
    • usePreserMember():预处理就诊人,迁移涉及患者缓存的逻辑
    • publicFn.preserMember() 逻辑替换为 await usePreserMember()
    • queryBaseMemberList_V3 获取就诊人列表逻辑若用于初始化或预加载,建议使用 usePreserMember() 替代
    • useGetDefaultMember()useIsToAuthPage():就诊人与授权判断逻辑
    • useDomain():获取域名用于接口与资源拼接
    • Hook 替代接口:当发现代码中调用 queryMemberCardList_V3 且入参只有 memberId 时,可替换成 hook 中的 queryMemberCardList_V3
    • 启动过程在 App.vue 中通过多个 Hook 串联(版本检查、配置拉取、应用状态判断、登录、菜单初始化、WebSocket 等),迁移时按步骤落地:
    • 版本检查 -> 配置拉取 -> 应用状态 -> 登录 -> 菜单初始化 -> WebSocket(如启用)

    7.2 Store 使用(依赖 @kasite/uni-app-base)

    • 通过 @kasite/uni-app-base/store 暴露的辅助方法:
    • mapState({ token: 'token', currentUser: 'currentUser' })
    • mapMutations({ setCurrentUser: 'setCurrentUser' })
    • 迁移时保留对 currentUsermemberListtoken 等原字段的读写,不做字段名转换。
    • Token/OpenId 获取:禁止使用 uni.getStorageSync('token'|'openid'|'smallProOpenId')。必须通过 store.state.tokenstore.state.openIdstore.state.smallProOpenId 获取(需先 import { useStore } from 'vuex' 并在 setup 中 const store = useStore())。

    7.3 工具方法约定

    • utils/index.ts 暴露:
    • export { common }(来自 @kasite/uni-app-base/utils
    • export const menuClick(e, _this, skipWay = 'navigateTo'):菜单点击统一入口,迁移时所有功能入口点击尽量走此方法,保留原 dataset 字段读取约定(data-itemdata-item-parent)。
    • 依赖运行时 getApp().globalData 多处字段(如 hosIdchannelIdconfig.pageConfiguration),迁移时不要改动字段名。
    • 页面点击事件参数需保留 dataset 约定: vue <view :data-item="item" :data-item-parent="parent" @click="menuClickFn"></view>
  const menuClickFn = (e) => { menuClick(e, proxy); }

7.4 组件与 easycom

  • 项目启用 easycom:uni_modules 下的组件可直接在模板中使用,无需显式 importcomponents 注册。
  • 富文本:使用 mp-htmluni_modules/mp-html),在页面中直接写 <mp-html :content="html" :domain="domain" />
  • 注意低版本兼容:mp-html 的解析器已针对 system 进行了容错,迁移时无需额外处理。

7.5 构建与环境

  • vite.config.js
    • @ 指向项目根目录,请统一使用 @/... 路径
    • 开发代理:/api -> VITE_APP_PROXY_API_BASE_URL(H5)
    • 非 H5 平台请确保接口为绝对域名或通过 useDomain() 构造完整 URL
    • 代码混淆插件在特定平台与非开发环境启用,uni_modules/**/* 已排除
  • 环境变量位于 env 目录(使用 loadEnv),迁移时如需新变量请按现有 .env 约定添加。

7.6 单位与样式

  • 统一采用 upx;已有页面(如 homeSearch.vuehomePage.vue)已完成从 rpxupx 更正。迁移时保持一致。
  • 模板中避免直接使用 style="{{...}}" 的 WXML 写法,改为 :style="..."

7.7 引入路径与工具方法替换(针对本项目)

  • 迁移页面/组件时,所有 import 路径需根据当前 uni-app 项目结构调整,避免引用不存在模块(如 utils/publicFn.js)。统一从 utils/index.ts 引入已有工具方法。
  • 来院导航:统一使用 utils/index.ts 暴露的 getLocation,替换 publicFn.getLocation,保持原逻辑与字段不变。
  • 菜单点击:统一使用 utils/index.ts 暴露的 menuClick,替换 publicFn.menuClick,保留 dataset 传参约定(data-itemdata-item-parent)。
  • 工具方法替换(common 与 icon 严禁混淆)
    • common:必须使用解构导入 import { common } from '@/utils';。严禁使用 import common from '@/utils/common' 或默认导入。
    • icon:必须使用默认导入 import icon from '@/utils/icon';。严禁从 @/utils 导入(如 import { icon } from '@/utils' 是错误的)。
    • publicFn:必须使用解构导入 import { publicFn } from '@/utils';。严禁使用 import publicFn from '@/utils/publicFn' 或默认导入。
    • getAuthorize:将 util.getAuthorize.call(proxy) 替换为 common.getAuthorize
    • util:本项目不提供 util 对象。
    • publicFn.getMember 必须替换为 await useGetMember()(需从 @/hook 导入)。
    • 其他 publicFn.xxxutil.xxx 方法(如 countDownformatTime 等),必须commonpublicFn 中寻找对应方法替换(需 import { common, publicFn } from '@/utils')。
    • getMember:涉及获取就诊人的逻辑,统一使用 hook。
  • 其它工具方法优先从 utils/index.ts@kasite/uni-app-base 提供的工具中引入,保持原字段不变,不做字段名转换。
  • 示例: ```ts // 正确示例: import { common, getLocation, menuClick } from '@/utils'; import icon from '@/utils/icon';

// 错误示例(严禁出现): // import {common} from '@/utils'; // 错误:common 需解构 // import { icon } from '@/utils'; // 错误:icon 未在 utils/index.ts 导出


### 7.8 接口封装调用规范(迁移需遵循)
- 目录结构:与 `pages/st1/service` 保持一致,建议按业务拆分为 `base/`、`home/`、`schemeDetail/` 等子目录,并在同级提供 `index.ts` 聚合导出。
- 统一封装:使用 `@kasite/uni-app-base` 提供的 `request` 与 `handle`。
  - **必须使用 New 版方法**:`handle.promistHandleNew` 和 `handle.catchPromiseNew`。
  - **严禁使用旧版**:`promistHandle` 和 `catchPromise`。
- **严禁引用不存在的 API 路径**:禁止使用 `config/api/...`,必须从 `@/pagesPersonal/service/...` 或各分包的 `service` 目录导入。
- **严禁依赖 api.ts 对象**:Service 层必须直接拼接 URL,严禁导入 `import api from '@/config/api'` 或使用 `api.Key` 形式。所有接口路径必须在 Service 文件内显式定义。
- **URL 拼接规范**:必须使用模板字符串 `${REQUEST_CONFIG.BASE_URL}wsgw/...` 进行拼接。
- **变量声明与 Handle 调用规范**:
  - 必须使用 `let resp` (严禁 `const`)。
  - 必须遵循 `let resp = handle.promistHandleNew(await request.doPost(...))` 结构 (注意 `await` 位置)。
- **返回值处理**:Service 层函数使用 `catchPromiseNew` 返回后,业务层调用**必须**使用解构获取 `resp` 和 `resData`。
  - **标准写法**:`let { resp, resData } = await apiName(params);`
  - **判断数据**:`if (common.isNotEmpty(resp)) { ... }`
- **命名冲突处理**:当导入的接口函数名与页面内的业务变量名冲突时,**必须**重命名页面内的局部变量。
- **串行调用与变量重命名**:当后续接口依赖前序接口结果时,应在 `if (common.isNotEmpty(resp))` 块内调用;为避免 `resp` 变量名冲突,后续接口调用**必须**使用解构重命名。
  - **标准写法**:`let { resp: newName } = await nextApiName(params);`
- **参考已完成页面**:`pages/st1/business/tabbar/homePage/homePage.vue` 和 `pages/st1/service/base/index.ts`。
- 域名拼接:接口地址统一从 `REQUEST_CONFIG.BASE_URL` 获取。
- **Service 封装示例**:
  ```ts
  import { REQUEST_CONFIG } from '@/config';
  import { request, handle } from '@kasite/uni-app-base';

  export const articleTypeListUrl = async (queryData: any) => {
    let resp = handle.promistHandleNew(
      await request.doPost(
        `${REQUEST_CONFIG.BASE_URL}wsgw/article/type/List/callApiJSON.do`,
        queryData
      )
    );
    return handle.catchPromiseNew(resp, () => resp);
  };
  • 业务层调用示例: ```ts import { articleTypeListUrl } from '@/pages/st1/service/base';

const getData = async () => {

let { resp, resData } = await articleTypeListUrl({
   ChannelId: app.globalData.channelId
});
if (common.isNotEmpty(resp)) {
   console.log('Data:', resp);
}

};

- 聚合导出:在 `service/index.ts` 中统一 `export * from './base';`。
- 引入路径:统一使用 `@` 别名。

### 7.9 页面实例与App实例获取规范
- **统一获取方式**:在 `<script setup>` 顶部统一获取 `proxy` 和 `app` 实例,严禁在函数内部重复调用 `getCurrentInstance()` 或 `getApp()`。
- **规范示例**:
  ```ts
  <script lang="ts" setup>
  import { getCurrentInstance } from 'vue';
  
  // 1. 获取当前组件实例代理
  const { proxy } = getCurrentInstance();
  // 2. 获取 App 全局实例
  const app = getApp();
  
  // 业务逻辑中使用
  const handleClick = () => {
      // 使用 proxy
      console.log(proxy);
      // 使用 app
      console.log(app.globalData);
  }
  </script>
  • 禁止行为
    • 禁止在方法内部定义 let app = getApp()
    • 禁止多次调用 getCurrentInstance()
    • 禁止使用 this (setup 中 this 为 undefined),应使用 proxy

8. 典型迁移示例

8.1 页面(WXML/JS → Vue + Hook)

<template>
  <view>
    <view v-if="list.length === 0">暂无数据</view>
    <view v-else v-for="(item, i) in list" :key="i" :data-item="item" @click="menuClickFn">
      {{ item.MenuName }}
    </view>
  </view>
</template>

<script lang="ts" setup>
import { ref, getCurrentInstance } from 'vue';
import { useOnLoad } from '@/hook';
import { common, menuClick } from '@/utils';
const { proxy } = getCurrentInstance() as any;
const app = getApp();
const list = ref<any[]>([]);
useOnLoad(async (options) => {
  // 迁移 onLoad 逻辑,保持原字段
  list.value = common.deepCopy(uni.getStorageSync('menuList') || []);
});
const menuClickFn = (e) => { menuClick(e, proxy); };
</script>

8.2 组件(Component → Vue 组件)

<template>
  <view>
    <view v-for="(n, i) in flowListRender" :key="i" @click="toggle(i)">{{ n.NodeTitle }}</view>
  </view>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
const props = defineProps<{ flowList: any[]; pageType: string }>();
const flowListRender = ref<any[]>([]);
watch(() => props.flowList, (val) => {
  // 保留原字段与处理逻辑
  flowListRender.value = (val || []).map((it) => ({ ...it, ShowContInfo: true }));
}, { immediate: true });
const toggle = (i: number) => { flowListRender.value[i].ShowContInfo = !flowListRender.value[i].ShowContInfo; };
</script>

9. 迁移流程建议(确保不影响现有逻辑)

  • 逐页迁移:按页面重要性与依赖顺序迁移,优先首页与公共组件
  • 模板先行:先将 WXML 改为 Vue 模板写法,完成指令、事件、绑定转换
  • Hook 接入:将页面 onLoad/onShow 逻辑接入 useOnLoad/生命周期,保留原字段与流程
  • 事件与菜单:统一接入 utils/index.tsmenuClick,保留 dataset 字段约定
  • 样式统一:单位统一改为 upx;背景图与资源路径按当前项目约定修正
  • 验证:运行对应平台,检查页面渲染、接口请求、跳转与交互;对复杂逻辑添加简要中文注释