Vue3+TypeScript+Uniapp构建医疗小程序全流程实战

发布时间:2026/9/8 22:56:46
Vue3+TypeScript+Uniapp构建医疗小程序全流程实战 简介面向使用 Vue3、TypeScript 与 Uniapp 开发跨端小程序的开发者这份完整医疗挂号小程序案例覆盖首页、预约挂号、时段选择、个人中心、视频信息等核心业务模块并配有类型声明、请求封装、页面路由与构建配置等工程化内容。资源共 30 个文件以 vue 页面组件、ts 逻辑脚本、json 配置文件为主辅以 png 图标、scss 样式、html 入口及 gitignore压缩包仅 1.48MB目录层级清晰便于对照源码逐模块学习。目前已有 2257 人浏览学习。通过该案例可掌握 Vue3 组合式 API 与 TypeScript 在小程序中的具体落地方式理解 uniapp 项目的类型约束、接口调用写法以及页面组织思路并能直接参考其登录注册、预约流程等常见医疗场景实现快速搭建可复用的小程序业务骨架适合希望从零上手 uniapp 工程化开发的初中级开发者。 写这篇东西的起因很直接我们团队要上一款医疗类小程序功能包含预约挂号、在线问诊、报告查询和支付需求一上来就要求“微信小程序能跑后面 App 和 H5 也得能跟上”。技术选型时没有太多犹豫定的是 Vue3 TypeScript Uniapp 这套组合。说实话Uniapp 的 Vue3 版本刚出来那会儿我也踩过不少坑但只要你把工程骨架、请求层、类型约束和跨端边界都提前想清楚这套组合在医疗这种“业务重、审核严、多端要同步”的项目里开发效率确实很能打。这篇文章不想列 API 清单而是按一个完整医疗小程序的落地顺序走一遍先讲清楚为什么这么选型再讲初始化、工程骨架、业务实现、跨端适配、上架发布最后把一些真实踩过的坑以“排查链路”的方式放出来。适合两种人一种是想从零开始做小程序的前端开发另一种是已经在用 Uniapp 但想上 Vue3TS、又怕踩坑的团队。所谓“一篇文章精通”不是让你背会所有接口而是让你知道一个像样的医疗小程序从头到尾是怎么拼出来的以及每一块为什么是长这样的。1. 先聊选型医疗小程序为什么是 Vue3 TS Uniapp而不是原生或 Taro1.1 医疗小程序的业务复杂度决定了它不是“玩具项目”很多人觉得小程序无非就是列表页加详情页真做了医疗项目才发现完全不是这么回事。一个预约挂号流程就要涉及科室树、医生排班、号源余量、就诊人选择、订单状态流转在线问诊又牵扯到会话、消息类型、图文上传、支付报告查询还要处理 PDF、图片、不同报告类型的预览兼容。这些业务有一个共同特点数据结构复杂、状态流转多、接口字段稳定但数量庞大。医疗项目还有一个特殊点审核严格上线后不能随意改版一旦页面逻辑混乱出了问题用户投诉和合规风险都会接踵而来。这就决定了前端代码不能是“能跑就行”的状态必须有一套清晰的组织方式。Vue3 的组合式 API 在这种场景下优势很明显它会强制你把一个业务的所有逻辑聚合在一起比如“号源锁定”相关的状态、计算属性、事件处理写在一处而不是像 Vue2 选项式那样散落在 data、created、methods、watch 各个角落。1.2 三个核心技术选型的关键比较先说 TypeScript。医疗数据模型的字段又多又长一个医生对象可能包含姓名、职称、科室编码、排班列表、擅长描述、头像地址等几十个字段。如果全是 JavaScript 的普通对象后端改一个字段名前端只能在运行时报错才发现有了 TypeScript 的 interface 做约束改完字段后编译阶段就能定位到所有引用点。打个比方JS 是口头说“这个人姓张”TS 是递给你一张带照片、带身份证号的证件谁冒充谁一目了然。再说 Uniapp 的跨端能力。医疗类产品很少只做微信小程序一般还要求支付宝小程序、公众号 H5、甚至原生 App。这里我做了一个选型对比给当时团队讨论用方案多端覆盖上手成本类型安全发布渠道适用场景原生微信小程序仅微信低弱微信平台只做单一平台、追求极致性能Taro微信支付宝H5中React 体系强多端团队熟悉 React愿意折腾Uniapp微信支付宝H5App低Vue 体系中多端应用市场多端同步、团队熟悉 VueFlutteriOSAndroidH5高强应用市场偏 App不太适合小体量小程序Uniapp 的另一个隐性优势是生态。虽然它的组件质量参差不齐但像海报生成、图表、签名板这类在医疗场景里比较实用的组件插件市场基本都能找到省去很多造轮子的时间。云打包和本地打包能力也成熟上架安卓各应用市场时省不少事。1.3 这套组合的隐藏成本与边界作为过来人我必须把话说清楚Uniapp 的 Vue3 版本不是没有代价。它基于 Vite 构建对插件的兼容性要求更高插件市场里有一部分老组件还是 Vue2 时代的写法拿过来直接在 Vue3 里用会报错。TypeScript 在 script 里类型提示很强但模板里的类型推断偶尔会失灵遇到问题别钻牛角尖该写类型断言就写类型断言。跨端也从来不是“一次编写处处运行”那么理想。微信小程序、H5、App 在原生能力、路由表现、生命周期上都有差异工程里必须用条件编译手段去处理这些差异。医疗项目尤其要记住一点核心业务逻辑尽量放在公共层只有涉及平台特有能力时才写条件编译分支不要让#ifdef散落得到处都是否则后期维护成本会直线上升。2. 初始化流程从创建项目到在微信开发者工具里跑起来2.1 环境准备Node、VSCode、微信开发者工具、Volar我强烈建议别用 HBuilderX 来做 Vue3TS 项目的日常开发它内置的 TS 类型提示和 ESLint 支持跟 VSCode 比差距明显。正确做法是VSCode 写代码HBuilderX 只用来做云打包或者干脆全部走 CLI 方案。VSCode 里必须装两样东西VolarVue Language Features和 TypeScript Vue Plugin装完之后记得禁用旧的 Vetur否则两个插件会打架模板里的类型提示和格式化都会乱掉。Node 版本建议 16 或 18 以上Vite 对旧版 Node 支持不好。微信开发者工具也要提前装好并扫码登录这一步很多人忽略CLI 方式运行时如果开发者工具没有登录命令行会一直卡住或静默失败。另外 VSCode 的终端类型尽量用 Git Bash 或 PowerShellCMD 在 Windows 上处理 npm 脚本偶尔会出现编码问题别在这种地方浪费时间。2.2 两条创建项目路径我推荐 CLI创建 Vue3 TypeScript 版本的 Uniapp 项目最标准的命令是npx degit dcloudio/uni-preset-vue#vite-ts medical-app cd medical-app npm install npm run dev:mp-weixin项目跑起来后微信开发者工具选择“导入项目”目录指向dist/dev/mp-weixin即可。这里有个细节微信开发者工具的 AppID 可以先选择测试号但后面要上线就必须在 manifest.json 里换成真实的小程序 AppID。HBuilderX 的创建方式则是新建项目时选 Vue3 模板界面操作确实简单但它生成的是 HBuilderX 专属目录后续想在 VSCode 里舒服地写 TS、想自己接管构建配置就会比较别扭。团队协作场景下CLI 项目可以完整纳入 Git 管理依赖和脚本都能统一CI/CD 也更容易接入所以如果条件允许直接上 CLI 方案。2.3 “运行到微信开发者工具上没反应”的完整排查链路这是新手问得最多的问题。我遇到过一次最后排查下来是开发者工具的服务端口没有打开。现在把排查链路完整写下来按这个顺序走基本都能解决确认微信开发者工具登录状态。未登录时 CLI 无法自动拉起来工具命令行可能没有任何报错。打开服务端口。微信开发者工具 → 设置 → 安全设置 → 服务端口必须是开启状态否则命令行无法通过本地 WebSocket 协议通知工具加载项目。检查是否是新版工具的“自动打开”功能被系统拦截。macOS 或 Windows 防火墙偶尔会拦截 localhost 通信如果开发者工具闪一下就没反应看下防火墙是否放行。手动导入兜底。直接在开发者工具里“导入项目”目录选dist/dev/mp-weixin这是最笨但最稳的方法能区分是自动拉起的问题还是编译产物本身的问题。确认编译产物存在。如果dist/dev/mp-weixin目录为空或者缺少app.json说明编译失败需要回到终端看报错常见原因是依赖没装全、Node 版本太老。这五步里前两步解决了 80% 的“没反应”问题。还有一个小技巧CLI 跑起来后终端会进入 watch 模式改代码自动重新编译但微信开发者工具偶尔不会自动刷新这时手动点一下工具里的“编译”按钮就好。3. 工程骨架搭建请求封装、路由、状态管理和类型声明3.1 一个可以直接复用的目录结构项目骨架决定了后期维护的舒适度我习惯这样组织src/ ├── api/ # 按业务域拆分的接口定义 │ ├── user.ts │ ├── registration.ts │ └── report.ts ├── components/ # 业务通用组件 ├── pages/ # 小程序页面 │ ├── home/ │ ├── register/ │ ├── consultation/ │ └── mine/ ├── stores/ # Pinia 状态 ├── types/ # 全局类型定义 │ ├── api.d.ts │ ├── doctor.ts │ └── order.ts ├── utils/ # 工具函数 │ ├── request.ts │ └── format.ts ├── static/ # 静态资源 ├── App.vue ├── main.ts ├── manifest.json ├── pages.json └── tsconfig.json页面目录按业务域分而不是按类型分这是我个人的执念。一个医疗项目页面数量动辄三四十个如果全堆在 pages 根目录下找人找半天按 home、register、consultation、mine 这种业务域拆分后一个挂号流程涉及的所有页面都在同一个文件夹里互相引用的路径也清晰。3.2 请求层为什么要单独封装如果不做封装每个页面直接调 uni.request你会面临三个很现实的问题第一接口地址在十来个页面里各写各的环境切换要改一堆文件第二token 过期后每个页面都要自己判断跳登录第三错误提示风格不统一有的页面弹 toast有的页面静默失败。我封装的 request.ts 核心思路很简单用 Promise 包裹 uni.request结合 TypeScript 泛型让每个接口函数都有明确的返回值类型。核心代码大概长这样// utils/request.ts const BASE_URL import.meta.env.VITE_API_BASE_URL || /api interface ResponseDataT { code: number message: string data: T } export function requestT(options: UniApp.RequestOptions): PromiseT { return new Promise((resolve, reject) { const token uni.getStorageSync(token) uni.request({ url: ${BASE_URL}${options.url}, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : , ...options.header, }, success: (res) { const response res.data as ResponseDataT if (response.code 0) { resolve(response.data) } else if (response.code 401) { // 登录态过期清空本地登录信息跳转登录页 uni.removeStorageSync(token) uni.removeStorageSync(userInfo) uni.navigateTo({ url: /pages/login/index }) reject(new Error(response.message)) } else { uni.showToast({ title: response.message, icon: none }) reject(new Error(response.message)) } }, fail: (error) { uni.showToast({ title: 网络异常请稍后重试, icon: none }) reject(error) }, }) }) }然后在 api 目录里定义业务接口时泛型的好处就体现出来了// api/doctor.ts import { request } from /utils/request export interface Doctor { id: string name: string title: string departmentId: string avatar: string } export function getDoctorList(departmentId: string) { return requestDoctor[]({ url: /doctor/list?departmentId${departmentId}, }) }页面里调用getDoctorList后拿到的doctors数组每一项都有完整的类型提示后端字段变了编译阶段就能发现哪些地方没跟上。医疗数据敏感还有一条附加纪律不要在拦截器或其他任何地方把 token 打到 console 里否则一次调试忘记删代码带着 token 日志上了生产被用户看到就是事故。3.3 路由与动态标题pages.json 的配置思路Uniapp 小程序没有 vue-router路由体系以 pages.json 里的页面栈为核心。每个要跳转的页面都必须先在 pages.json 注册否则跳转会报 “page not found”。tabBar 页面只能放在 pages 数组的第一层而普通页面可以任意嵌套这两个概念要分清。页面跳转传参是通过 URL 查询串实现的uni.navigateTo({ url: /pages/register/doctor-list?deptId${deptId}deptName${encodeURIComponent(deptName)}, })目标页面在onLoad(options)里拿参数注意 options 里都是字符串需要手动转类型。为什么强调encodeURIComponent(deptName)因为医疗场景里科室名经常带“”“·”这类特殊符号不编码的话参数会被截断这是高频坑。动态设置导航栏标题也是医疗项目里的常见需求。比如报告详情页需要根据报告类型展示“检验报告”“影像报告”不同名称直接在 onLoad 里调用uni.setNavigationBarTitle({ title: 检验报告 })即可。注意要在页面显示前调用放在 onLoad 里最合适如果页面已经被压入栈底再想改标题就需要配合uni.$emit或事件总线了。3.4 状态管理选 Pinia但注意持久化Vue3 项目状态管理没有悬念选 Pinia。它比 Vuex 轻量、对 TypeScript 支持更友好而且去掉了 mutations 一层写起来少很多模板代码。医疗小程序我至少会拆两个 storeuserStore 管用户信息、就诊人列表、登录态orderStore 管挂号订单、问诊订单的状态。在 Uniapp 里用 Pinia 有一个注意点小程序页面销毁后内存中的 store 状态不会丢但小程序被微信回收后store 会被重置。所以关键状态必须持久化到 Storage。我的做法是登录成功后把 userInfo 写进uni.setStorageSync(userInfo)在 App.vue 的 onLaunch 里读回并写进 store这样用户冷启动后登录态还在。也可以用插件做自动持久化但医疗项目我对自动化的东西比较谨慎手工管理更可控。4. 业务页面拆解挂号、问诊、报告一个典型流程的完整实现4.1 预约挂号最典型的多步流程与参数传递挂号流程是医疗小程序的“门面”也是页面间通信最密集的场景。完整链路是选择科室 → 选择医生 → 选择日期与号源 → 确认就诊人 → 提交订单 → 支付/完成。这里最有讨论价值的是“科室 → 医生列表”这一跳的参数传递。从科室列表进入医生列表需要带deptId科室编码和deptName科室名称科室名称用于医生列表页顶部展示。deptName 是中文必须编码传参uni.navigateTo({ url: /pages/register/doctor-list?deptId${item.id}deptName${encodeURIComponent(item.name)}, })医生列表页接收onLoad(options: Recordstring, string) { this.deptId options.deptId this.deptName decodeURIComponent(options.deptName || ) uni.setNavigationBarTitle({ title: this.deptName }) }这里把解码放在页面上做而不是传参前做是因为 decode 应该在离展示最近的地方避免某些平台自动编码导致二次编码出错。号源选择组件是另一个坑。早上的号和下午的号往往价格不同医生排班数据是从接口返回的数组前端要做的是把“老年号”“普通号”“专家号”按时间段渲染成格子并处理“约满”“停诊”几种状态。我的实现思路是先写一个ScheduleCell组件接收status和time两个 props内部根据状态切换主题色和禁用态父组件用 computed 把原始排班数据按日期分组渲染成多行格子。医疗项目里这种“状态多、分支多”的 UI 组件TypeScript 的联合类型特别有用type ScheduleStatus available | locked | full | closed interface ScheduleCellProps { time: string status: ScheduleStatus }4.2 在线问诊长列表、会话窗口与消息重连在线问诊本质是一个轻量 IM。页面结构是消息列表 底部输入区关键点在于小程序里长列表渲染大量消息时直接用 v-for 渲染几千条会导致明显掉帧。我建议按页加载每次加载 20 到 30 条历史消息配合scroll-view的滚动事件做上拉加载。消息数据模型用 TypeScript 定义好之后渲染就不用反复判断了interface ChatMessage { id: string sessionId: string senderType: doctor | patient messageType: text | image | system content: string createTime: number }WebSocket 连接在医疗问诊里要格外注意心跳。小程序端网络环境复杂断网重连频繁我的做法是连接建立后每 30 秒发一次 ping超过 50 秒没收到 pong 就主动断开重连页面 onHide 时暂停发送心跳onShow 时检查连接状态。这套机制不复杂但能避免新人在聊天页面遇到“消息发出去没反应”时无从下手。4.3 报告查询文档预览与动态标题的结合报告查询页面是“动态标题”的典型应用场景。报告分为检验报告、检查报告、影像报告等进入页面时根据reportType字段动态设置标题同时内部渲染不同的预览方式onLoad(options: Recordstring, string) { const reportTypeMap: Recordstring, string { lab: 检验报告, image: 影像报告, physical: 体检报告, } uni.setNavigationBarTitle({ title: reportTypeMap[options.type] || 报告详情 }) }图片类报告用uni.previewImage实现单图/多图预览本地路径和网络路径都可以PDF 类报告用uni.openDocument打开需要传 filePath。这里有一个经验后端返回的可能是https://...pdf或 base64 字符串如果是 base64 需要用uni.getFileSystemManager().writeFile先写成临时文件再交给 openDocument不能直接把 base64 塞给 openDocument这是新手最容易踩的坑。5. 跨端与发布manifest配置、隐私弹窗、分享、支付签名这些老技术点5.1 manifest.json一份需要反复确认的清单manifest.json 是 Uniapp 项目的“总开关”小程序端最关心的几个配置是appid微信小程序 AppID、项目名称、App 模块权限配置。如果你打算后续打包 App需要在这里勾选用到的原生模块比如蓝牙、相机、地图小程序端则不需要在这里配置而是在微信公众平台后台配置。权限申请的原则是“最小够用”医疗类小程序尤其如此权限越少审核越顺用户在授权弹窗时的恐慌感也越低。5.2 iOS 隐私政策弹窗与“不同意就退出”的实现如果项目要打包成 App 上架 iOS首次启动必须弹隐私政策与用户协议弹窗。弹窗本身不难在 App.vue 的 onLaunch 里检查是否有“已同意隐私”标记没有就弹窗难点在于“用户点了不同意怎么处理”。需求上通常是退出 App响应代码很直接uni.showModal({ title: 提示, content: 您未同意隐私政策无法继续使用本应用, showCancel: false, confirmText: 退出, success: (res) { if (res.confirm) { uni.exitApp() } }, })这里真正重要的是合规逻辑不是代码用户没同意之前不能初始化统计 SDK、不能上报任何用户信息、不能调用任何涉及隐私的 API。所以隐私弹窗的同意回调里才去初始化第三方 SDK不同意则直接退出这个顺序不能颠倒。5.3 onShareAppMessage 被全局方法覆盖问题出在哪热词里提到“uniapp onShareAppMessage 被全局方法覆盖”这个我太熟了。常见写法是在 App.vue 的 globalData 或 mixin 里写了一个全局分享配置希望所有页面都有分享能力但小程序分享生命周期是页面级的页面没定义onShareAppMessage时右上角菜单默认可能没有分享按钮而一旦在 mixin 里定义了全局的onShareAppMessage又会导致所有页面都走同一套分享配置个别页面想自定义标题和图片时发现“被覆盖了”。我的做法全局 mixin 提供默认配置但页面里需要自定义时必须在页面自身定义onShareAppMessage并显式返回当前页面的分享配置onShareAppMessage() { return { title: XX医院在线挂号, path: /pages/home/index, imageUrl: https://cdn.xxx.com/share.jpg, } }页面自身的生命周期优先级高于 mixin所以不会被覆盖。另外自定义分享按钮用button open-typeshare这个是微信内置能力不需要调用接口。5.4 扫码、支付与上架那些不能跳过的验证uni.scanCode扫码结果有时“扫出来是一串数字”这不是 bug。扫码接口返回的是一个普通字符串二维码、条形码都能扫条形码结果天然就是纯数字或混合数字关键在后端要定义好业务规则比如工作人员扫设备上的条形码前端把码值传给后端后端判断是设备 ID 还是单据号。前端不要自作主张去解析把它当普通字符串原样上报即可。微信支付 v3 对接前端要做的其实不多。后端统一下单后返回 5 个关键参数timeStamp、nonceStr、package、signType、paySign前端直接拿去调uni.requestPaymentconst paymentParams await createOrder(...) uni.requestPayment({ provider: wxpay, timeStamp: paymentParams.timeStamp, nonceStr: paymentParams.nonceStr, package: paymentParams.package, signType: paymentParams.signType, paySign: paymentParams.paySign, success: () uni.showToast({ title: 支付成功 }), fail: (err) uni.showToast({ title: 支付取消 }), })我的原则是前端绝不参与签名只做参数透传和结果展示。如果碰到“支付功能暂时无法使用”这类提示先想两种可能一是小程序后台支付权限没开通或存在违规未申诉二是后端签名算法有问题。开发层面能做的就是确保参数完整、签名正确而涉及违规限制正确姿势是自查小程序内容资质、走官方申诉流程而不是想方设法绕过这一点在医疗领域尤其要清醒。上架安卓应用市场我提一下大概流程先用 HBuilderX 做云打包或本地打包生成 apk/aab然后准备软件著作权证书、隐私政策链接、ICP 备案信息每个市场要求略有差异。有一个小提醒应用市场审核比小程序更关注隐私政策文本一定要写清楚收集哪些信息、如何使用、怎么注销账号这些文本建议让法务或懂合规的同事审一遍再提交。6. 一套值得带走的排查经验软键盘、路由参数、扫码返回数字这些真实问题6.1 微信小程序软键盘遮挡查询内容这是一个超级常见的体验问题。搜索框在页面顶部用户输入关键词后调起软键盘把下方的搜索结果列表挡住体验很差。默认情况下小程序 input 组件设置了adjust-position为 true键盘弹起会尝试把页面整体上推但有时候上推高度不够或者页面本身有滚动容器导致列表还是被盖住。我的处理方式是双保险第一input 上设置confirm-typesearch让用户点击键盘搜索键时触发bindconfirm第二在bindfocus和bindblur事件里手动调整页面滚动位置onInputFocus() { setTimeout(() { uni.pageScrollTo({ scrollTop: this.scrollTarget, duration: 200, }) }, 300) }scrollTarget是查询结果列表顶部距离页面顶部的位置可以用uni.createSelectorQuery().select(#resultList).boundingClientRect()拿到。这样即使键盘弹出列表也能被推到可视区域。6.2 路由参数里的中文与特殊符号这个问题前面已经埋过伏笔。挂号科室名、搜索关键词、报告类型这些参数如果直接拼在 URL 里传过去在 iOS 端偶尔能正常解析但在部分 Android 机型或者从分享链接进入时会乱码或截断。原因是某些平台对 URL 中的非 ASCII 字符做了重新编码前端没有统一处理就会出问题。解决方案就是传参时统一编码、接收时统一解码两条规则里漏一条都会出 bug。另外如果参数里带?、、这些 URL 保留字符更要编码否则参数会被拆开。写过一次这个 bug 之后我的团队定了规矩所有非数字类型的路由参数传的时候必须 encodeURIComponent收的时候必须 decodeURIComponent没有例外。6.3 关于分享方法的合并写法前面提到全局 mixin 和页面级 onShareAppMessage 的冲突这里给一个实用合并写法。在 mixin 里不要直接 return 死配置可以改成判断页面是否有自定义函数没有才用默认值// mixins/share.ts export default { onShareAppMessage(this: any) { const customShare this.customShareConfig return { title: customShare?.title || 默认标题, path: customShare?.path || /pages/home/index, imageUrl: customShare?.imageUrl || , } }, }页面里需要自定义分享时定义customShareConfig即可不需要重复写生命周期。这个方式兼顾了全局兜底和页面自定义比直接覆盖干净得多。6.4 技术之外的一点提醒在你一头扎进代码之前医疗类小程序有一个绕不开的基础类目资质。微信公众平台对医疗类目审核很严格在线问诊需要互联网医院执业资质报告查询要明确数据来源合规。产品经理和运营先把类目和资质准备齐全前端再投入开发否则页面做完才发现类目不通过等于白干。安全性方面医疗项目涉及大量敏感个人健康信息接口必须做签名和验签传输全程 HTTPS敏感字段加密。我见过一些团队花很多时间去研究逆向工具其实不如把网络层做强参数签名、防重放、权限校验做到位逆向成本高了安全风险自然就低了。我个人在这套流程里最深的体感是医疗小程序的开发难点从来不在某个 API 不会用而在于怎么把几十个页面、几十个接口、复杂的业务状态组织得井井有条。Vue3 TypeScript Uniapp 这套组合恰好能帮你把“井井有条”这件事落到实处。遇到拿不准的平台差异时先查官方文档再在开发者工具和真机上各验证一遍谨慎多一点线上事故就少一点。本文还有配套的精品资源点击获取