ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

智慧养老微信小程序毕设源码:原生JavaScript开发与工程实践解析

智慧养老微信小程序毕设源码:原生JavaScript开发与工程实践解析 简介基于JS开发的智慧养老微信小程序毕设源码面向计算机相关专业毕业生及需要项目实战练习的学习者也可用于课程设计或期末大作业。压缩包内共49个文件包含12个配置文件、10个JS逻辑文件、10个样式文件、9个页面结构文件、5张PNG与2张JPG图片及1个说明文档整体仅171KB各类型文件分别负责页面结构、交互逻辑、样式控制与素材资源模块划分清晰便于按需阅读与调试。项目围绕养老场景设计了用户首页、健康指标心跳、血压、体温、血氧、血糖等、捐赠功能、服务请求、家庭管理、管理员信息等多个页面覆盖智慧养老平台的主要功能链路。源码经过严格调试可正常运行已有65人学习下载既能作为毕业设计或课程设计的完整模板也可用于二次开发在短时间内理解微信小程序从页面配置到组件复用再到交互响应的实际开发流程。1. 智慧养老微信小程序源码想顺利毕业先把这个工程的边界看清楚智慧养老这几年是毕设选题里的热门方向但很多同学拿到的所谓“源码包”要么只做了几个静态页面要么接口全写死、换台电脑就跑不起来。这份基于 JavaScript 原生开发的小程序毕设工程核心价值在于把老人档案、健康数据、服务预约、消息推送这套闭环做全了页面跳转、TabBar、请求封装、本地缓存都按微信小程序的标准工程方式来组织不是那种打开只有三五个 WXML 的毛坯版本。不管你是打算直接拿去毕业答辩还是想在这个基础上二次开发、加上自己的功能模块这份工程都能帮你省掉从零搭框架的时间。适合的人群很明确正在找毕设题目的学生、想快速理解小程序原生开发套路的初级开发者以及需要一份能完整演示给导师看、能把代码逻辑讲清楚的项目源码。2. 目录结构与数据流设计拿到工程后第一件事不是读代码是先看它怎么组织2.1 源码包里的目录组织与页面职责打开工程后你会发现这是标准的微信小程序原生项目结构没有 npm 依赖解压后丢进微信开发者工具就能直接编译。核心目录大概是这样划分的project-root/ ├── app.js # 全局逻辑登录态初始化与全局数据管理 ├── app.json # 全局配置页面注册、TabBar 配置、window 样式 ├── app.wxss # 全局样式公共 CSS 变量与基础样式 ├── utils/ │ ├── request.js # wx.request 统一封装处理 token 与业务状态码 │ ├── auth.js # 登录态管理wx.login 换 token、缓存读写 │ └── format.js # 时间/数值格式化工具函数 ├── api/ │ ├── health.js # 健康数据相关接口封装 │ ├── booking.js # 服务预约相关接口封装 │ └── user.js # 用户信息与家人绑定接口 ├── components/ │ ├── health-card/ # 健康数据卡片组件 │ ├── status-tag/ # 工单状态标签组件 │ └── empty-state/ # 空列表占位组件 └── pages/ ├── index/ # 首页老人档案卡片、今日健康概览、紧急求助 ├── health/ # 健康数据血压、心率、血糖的录入与趋势展示 ├── booking/ # 服务预约护理、送餐、代办的工单列表与状态切换 ├── message/ # 消息通知健康预警、工单状态变更推送 └── profile/ # 个人中心家属绑定、老人档案管理、演示模式入口拿到源码后第一件事我建议你先看app.json里pages数组的排列顺序。数组中第一项决定了小程序冷启动后加载哪个页面也就是通常说的首页。这个工程里排在第一位的是pages/index/index打开就能看到老人档案卡片和今日健康概览符合智慧养老产品“一屏看状态”的核心诉求。毕设项目的页面数量有讲究。5 个主页面加 3 个 Tab 页签属于“讲得完、演示得动”的典型配置。页面太少显得工作量不足页面太多你自己都记不清每个页面的逻辑答辩时被导师一问就乱了阵脚。这个工程里每个页面只负责一件事首页做信息聚合健康页做数据明细预约页做流程操作消息页做被动接收个人中心做关系维护。这种职责单一化的划分方式也是后续二次开发时最重要的维护边界。2.2 为什么选原生 JavaScript 而不是 uni-app 或 Taro这个问题几乎在每次答辩都会被问到。微信小程序原生 JavaScript 开发确实有一种“远古时期”的味道没有热重载、没有 TypeScript 补全、连跨页面传参都要靠 URL query 或者全局缓存。但对毕业设计来说原生 JavaScript 有三个无可替代的价值这也是我拿到这套工程后第一判断就是“不用迁移框架”的原因。第一是零依赖。整套源码里没有node_modules不需要 npm install不需要配 webpack 或 vite 构建链。你只需要安装一个微信开发者工具把工程目录导进去点编译页面就出来了。uni-app 和 Taro 的工程光环境配置就能折腾半天装包时版本冲突、编译时报错、运行时报错每一层都是不确定因素。毕设答辩前夜你恐怕不想花在排环境问题上。第二是定位问题效率高。原生小程序的报错直接指向具体文件和行号而且微信开发者工具的调试器、Network 面板、Storage 面板都是原生级支持。换成跨端框架代码经过编译映射运行时错误指向的可能是编译产物你还要在 sourcemap 里来回找对应关系。对于需要现场改代码演示的场景原生 JS 改完保存模拟器立刻刷新不用等编译。第三是概念模型清晰。小程序原生开发只要讲三件事WXML 写结构、WXSS 写样式、JS 写逻辑数据通过 setData 从逻辑层传到渲染层。任何评委都能听懂你在做什么。而 uni-app 这类框架你会被迫解释“编译时”“运行时”“条件编译”这些概念解释成本高而且容易被追问细节答不上来。原生开发当然有代价这套代码不能直接复用做 H5 或 App。如果你的开题报告里写的是“多端覆盖”那你可能需要三思。不过话说回来大学毕业设计的评分标准看重的是你“做完了一个东西且能讲清楚”不是每年开源中国评选最佳项目。把精力集中在把小程序这端做扎实比铺开三端但每端都半成品要划算得多。2.3 全局配置与请求封装读懂 app.json 和 utils 里的基础代码解压后先打开app.json这是整个小程序的地基。里面有两个节点最值得关注window和tabBar。{ pages: [ pages/index/index, pages/health/health, pages/booking/booking, pages/message/message, pages/profile/profile ], window: { navigationBarBackgroundColor: #0a8f77, navigationBarTextStyle: white, navigationBarTitleText: 智慧养老服务平台, backgroundColor: #f5f6f7 }, tabBar: { color: #8a8a8a, selectedColor: #0a8f77, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/health/health, text: 健康 }, { pagePath: pages/booking/booking, text: 预约 }, { pagePath: pages/message/message, text: 消息 }, { pagePath: pages/profile/profile, text: 我的 } ] } }几个参数的说明navigationBarBackgroundColor控制顶部导航栏背景色这里用的墨绿色是智慧养老类产品常见的视觉主色调强调沉稳和信任感。navigationBarTextStyle只有white和black两个合法值配深色背景时用white配浅色背景时用black。tabBar.list长度限制是 2 到 5 个这个工程用了 5 个已经到上限。特别注意pagePath必须和pages数组里注册的路径完全一致漏注册或路径写错编译阶段直接报错这也是毕设工程里“打开就白屏”的最大来源之一。接着看utils/request.js里的网络请求封装。这个文件是整个工程和后端通信的咽喉要道看懂它就明白了所有页面请求数据的基本模式// utils/request.js const BASE_URL https://your-api-server.com/api function request(path, options {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token) wx.request({ url: BASE_URL path, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { // 约定后端统一返回 { code, data, msg } 结构 if (res.data.code 200) { resolve(res.data.data) } else if (res.data.code 401) { // token 失效清理缓存并跳转登录页 wx.removeStorageSync(token) wx.removeStorageSync(userInfo) wx.navigateTo({ url: /pages/login/login }) reject(new Error(登录态已过期)) } else { wx.showToast({ title: res.data.msg, icon: none }) reject(new Error(res.data.msg)) } }, fail: (err) { wx.showToast({ title: 网络请求失败, icon: none }) reject(err) } }) }) } module.exports { request }代码背后的设计考量值得说清楚。BASE_URL是后端服务的根地址毕业设计如果是在本地起后端这里要填电脑的局域网 IP 加端口形如http://192.168.1.100:8080/api真机预览时必须填电脑的局域网 IP 而不是 localhost。Authorization用的Bearer token格式是当前最主流的选择后端只需要解析 JWT 就能拿到用户身份。统一判断res.data.code是为了把业务错误和网络错误区分开——后端返回业务提示不算网络失败不能走fail分支。在这个基础上接口层再按业务模块封装一次// api/health.js const { request } require(../utils/request) function getHealthRecords(elderId, page 1) { return request(/health/records?elderId${elderId}page${page}, { method: GET }) } function submitHealthRecord(data) { return request(/health/records, { method: POST, data }) } module.exports { getHealthRecords, submitHealthRecord }页面里引用接口时只需要关心业务参数不用重复处理 token 追加和错误码判断。这种“工具函数 → 接口封装 → 页面调用”的三层架构是答辩时非常好讲的设计。导师问你接口怎么管理的你就指这三层说工具层管网络协议、接口层管业务语义、页面层管交互反馈每一层只干一件事改动不会跨层扩散。3. 核心功能链路登录鉴权、健康档案与服务预约的三个关键实现3.1 登录鉴权wx.login 换 token 的完整闭环智慧养老场景有个特殊性使用小程序的人是家属或护理人员服务的对象是老人。这个工程的登录逻辑采用wx.login获取临时凭证后端拿凭证去微信服务器换取 openid再在自己的用户表里匹配身份。整个流程前端只做一件事拿到 token存起来。// utils/auth.js const TOKEN_KEY token const USER_KEY userInfo function login() { return new Promise((resolve, reject) { wx.login({ success: (res) { if (!res.code) { reject(new Error(获取 code 失败)) return } // res.code 是临时凭证5 分钟内有效且只能使用一次 wx.request({ url: ${getApp().globalData.baseUrl}/api/auth/login, method: POST, data: { code: res.code }, success: (resp) { if (resp.data.code 200) { const { token, userInfo } resp.data.data wx.setStorageSync(TOKEN_KEY, token) wx.setStorageSync(USER_KEY, userInfo) getApp().globalData.userInfo userInfo resolve(userInfo) } else { reject(new Error(resp.data.msg)) } }, fail: reject }) }, fail: reject }) }) } function getToken() { return wx.getStorageSync(TOKEN_KEY) } function logout() { wx.removeStorageSync(TOKEN_KEY) wx.removeStorageSync(USER_KEY) } module.exports { login, getToken, logout }这里必须强调一个概念区别wx.login返回的code只是一个临时票据它本身不携带任何用户身份后端必须拿着这个 code 去微信的jscode2session接口换取 openid 和 session_key再用 openid 查自己的用户表。很多毕设后端直接把 code 当账号存在数据库里这是完全错误的理解。前端只管把 code 传给后端后端验证通过后签发自家的 token 返回前端后续请求都带着这个 token。登录动作挂在app.js的onLaunch生命周期里执行// app.js App({ globalData: { baseUrl: https://your-api-server.com, userInfo: null, emergencyPhone: 120 }, onLaunch() { const token wx.getStorageSync(token) if (token) { // 已有 token静默校验有效性 this.checkSession() } else { // 无 token走完整登录流程 login() .then(() this.initAppData()) .catch((err) console.error(登录失败, err)) } } })用缓存存储 token 的核心好处是用户第二次打开小程序时直接跳过登录弹窗体验接近真实 C 端产品。毕设演示最怕每次打开都要重新扫码登录很拖节奏。有缓存之后登录一次演示全程畅通。3.2 健康数据上报与列表渲染从表单提交到日期分组健康档案是智慧养老小程序的核心模块。老人的血压、心率、血糖数据可以由家属手动录入也可以由蓝牙设备自动上传。这个工程里采用的是手动录入加列表展示的方式既保证了演示可控又展示了表单交互和数据组织能力。先看录入页面的逻辑// pages/health/add-health.js Page({ data: { form: { systolic: , diastolic: , heartRate: , bloodSugar: , measuredAt: }, saving: false }, onInput(e) { const field e.currentTarget.dataset.field this.setData({ [form.${field}]: e.detail.value }) }, onPickerChange(e) { this.setData({ form.measuredAt: e.detail.value }) }, async onSubmit() { const { systolic, diastolic, heartRate } this.data.form if (!systolic || !diastolic || !heartRate) { wx.showToast({ title: 请填写血压和心率, icon: none }) return } this.setData({ saving: true }) try { const result await submitHealthRecord(this.data.form) wx.showToast({ title: 保存成功, icon: success }) setTimeout(() wx.navigateBack(), 500) } catch (error) { wx.showToast({ title: error.message, icon: none }) } finally { this.setData({ saving: false }) } } })几个关键点拆开讲。onInput里用e.currentTarget.dataset.field获取输入框绑定的字段名好处是四个输入框共用一个处理函数不用写四个独立的事件回调。setData支持字符串路径form.systolic这种写法只会局部更新对象里的一个字段不会整体替换form性能上更优。saving状态位防止用户双击提交按钮产生重复记录这是真实产品里必须考虑的并发场景答辩讲到这能加分。列表页的渲染重点是数据分组逻辑。健康记录按日期归组同一天的多条记录显示在一起这是医疗健康类应用最标准的展示模式// pages/health/health.js Page({ data: { records: [], groupedByDate: [], abnormalCount: 0 }, onShow() { this.fetchRecords() }, async fetchRecords() { const elderId wx.getStorageSync(currentElderId) const records await getHealthRecords(elderId) const grouped this.groupByDate(records) const abnormal records.filter((r) this.isAbnormal(r)) this.setData({ records, groupedByDate: grouped, abnormalCount: abnormal.length }) }, groupByDate(records) { // 按 measuredAt 的日期部分归组同一日期下的记录保持原有顺序 const map {} records.forEach((r) { const date r.measuredAt.split( )[0] if (!map[date]) map[date] [] map[date].push(r) }) return Object.keys(map).map((date) ({ date, list: map[date] })) }, isAbnormal(r) { // 收缩压超过 140 或 舒张压超过 90 均视为异常 return Number(r.systolic) 140 || Number(r.diastolic) 90 } })groupByDate的思路简单有效遍历所有记录按measuredAt字段的日期部分塞进一个对象作为分组桶最后再把对象转成数组。WXML 端用两层wx:for渲染外层遍历分组内层遍历每条记录。isAbnormal方法做异常阈值判断只要收缩压超过 140 就标红。这两块逻辑拼起来健康页就同时具备了“数据展示”和“异常提示”的能力。3.3 服务预约与工单状态流转前端状态机怎么设计服务预约这个模块涉及到工单的状态流转是整个工程里最容易写乱的地方。这个工程用一组数字枚举来管理状态每个数值对应一种业务状态。看下面这个状态定义// api/booking.js 里定义的状态枚举 const ORDER_STATUS { PENDING: { value: 0, label: 待接单, color: #ff9800 }, ACCEPTED: { value: 1, label: 已接单, color: #0a8f77 }, IN_PROGRESS: { value: 2, label: 服务中, color: #2196f3 }, FINISHED: { value: 3, label: 已完成, color: #8a8a8a }, CANCELLED: { value: 4, label: 已取消, color: #f44336 } }状态从 0 到 4 依次流转每个状态对应一个界面标签色。前端页面的核心工作是根据当前状态决定按钮的文案和操作是否能点// pages/booking/booking.js Page({ data: { orders: [], activeTab: all, statusTabs: [ { key: all, label: 全部 }, { key: pending, label: 待接单 }, { key: ongoing, label: 进行中 }, { key: finished, label: 已完成 } ] }, onPullDownRefresh() { this.fetchOrders().finally(() wx.stopPullDownRefresh()) }, async fetchOrders() { const orders await getOrders({}) this.setData({ orders: this.filterOrders(orders) }) }, filterOrders(orders) { if (this.data.activeTab all) return orders const statusMap { pending: [0], ongoing: [1, 2], finished: [3] } return orders.filter((o) statusMap[this.data.activeTab].includes(o.status)) }, onCancelOrder(e) { const { id } e.currentTarget.dataset wx.showModal({ title: 确认取消, content: 取消后订单将无法恢复, success: async (res) { if (res.confirm) { await cancelOrder(id) wx.showToast({ title: 已取消, icon: success }) this.fetchOrders() } } }) } })这段代码里最值得注意的设计是filterOrders方法里的statusMap。页签的ongoing同时映射了已接单和服务中两个状态值因为这两个状态在用户视角里都属于“进行中”。如果不用映射表而是直接在业务代码里写判断条件后面加一个“待评价”状态就要修改好几处的条件判断容易漏改。状态映射表把这种多对一的映射关系集中管理是状态机模式在小项目里最轻量的一种落地方式。取消操作加了wx.showModal二次确认弹窗这是电商类小程序的标准交互。毕设项目里一个工单取消没有任何确认直接执行演示时误触了就是一个尴尬瞬间。加上确认弹窗成本就两三行代码但产品完整度提升明显。4. 避坑指南真机预览、域名校验与时间格式化最容易翻车的五个细节4.1 坑一request 合法域名未配置真机请求全部失败现象 微信开发者工具里一切正常点“真机预览”后所有接口请求失败控制台报http://xxx 不在以下 request 合法域名列表中。原因 微信小程序明确要求生产环境的网络请求域名必须在小程序管理后台配置为合法域名且必须 HTTPS。开发者在开发者工具里可以勾选“不校验合法域名”来绕过这个限制但真机上没有任何豁免。毕业设计通常没有独立备案域名用的可能是本地局域网 IP 或 http 地址自然过不了校验。解决 如果你是个人主体无法注册企业小程序那就只能在开发者工具里调试或者申请微信云开发、云函数免域名。最务实的做法是答辩现场用开发者工具加“不校验合法域名”模式演示但提前在真机上安装体验版并开启调试模式微信允许在真机体验版上打开调试面板绕过域名校验。这一步必须在答辩前一晚用真实手机跑一遍确保登录和接口都能通。4.2 坑二wx.getUserProfile 调用时机不对导致无法获取用户信息现象 点击“微信授权登录”按钮授权弹窗没有出现或者一直转圈最后拿到的是匿名头像和昵称。原因 微信官方在基础库 2.21.2 之后收紧了用户信息获取规则wx.getUserProfile只能由用户点击按钮的tap事件直接触发不能在onLoad或onShow里异步调用否则直接失效。老的毕设代码很多还在用已经废弃的wx.getUserInfo这类代码在现在的基础库版本下获取到的全是匿名信息头像是一张灰色默认图昵称是“微信用户”。解决 把获取用户信息的代码挪到按钮的事件处理函数里。智慧养老场景其实不依赖微信头像昵称家属身份用姓名加手机号就能建立建议直接简化登录逻辑让用户手动输入姓名和手机号配合wx.login换 token 使用。这样既绕开了微信用户信息授权限制又符合养老场景的真实交互——年轻人可以点微信授权老人使用时的录入流程反而更简单直接。4.3 坑三真机上图片加载失败开发者工具却显示正常现象 同一张图片在开发者工具里正常显示真机上一片空白控制台报Failed to load image。原因 图片资源地址是 http 协议或者本地图片路径写成了相对路径。真机上 image 组件的 src 对 http 资源有严格限制即使开了调试模式http://的图片也可能被拦截。另外一个隐蔽原因是本地图片放在了项目根目录之外的路径打包时没有被包含进去。解决 先分清楚图片是本地还是远程。本地图片统一放到images/目录WXML 里使用绝对路径写作/images/xxx.png从项目根目录开始写。远程图片一律用 HTTPS 地址如果是后端返回的 http 地址在后端做一次 URL 替换或在渲染前用replace将http:改成https:。我在处理这个工程时直接把所有远程图片替换为本地占位图确保演示时不依赖外网。4.4 坑四时间格式化后出现 undefined 或 NaNiOS 与 Android 显示不一致现象 健康记录列表里时间显示为undefined 05:20或NaN-NaN-NaN且同样一份记录在不同手机上显示格式不同。原因 后端返回的时间格式不统一有的接口返回 ISO 字符串2024-05-20T08:30:00.000Z有的返回时间戳还有的返回的是2024-05-20 08:30:00这种中间带空格、横杠分隔的格式。更关键的是 iOS 的 JavaScriptCore 引擎不支持new Date(2024-05-20 08:30:00)这种横杠格式直接解析失败返回 Invalid Date。解决 必须写一个统一的时间格式化函数把各种输入格式兼容掉// utils/format.js function formatTime(input) { let date if (typeof input number) { // 兼容 10 位秒级时间戳和 13 位毫秒级时间戳 date new Date(input 1e12 ? input * 1000 : input) } else if (typeof input string) { // 把横杠分隔的日期替换为斜杠兼容 iOS 解析规则 const normalized input.replace(/-/g, /) date new Date(normalized) } else { return } if (isNaN(date.getTime())) return const pad (n) (n 10 ? 0 n : n) return ${date.getFullYear()}-${pad(date.getMonth() 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())} } module.exports { formatTime }replace(/-/g, /)这一行是专门处理 iOS 兼容问题的。为什么 iOS 会解析失败因为 iOS 的 JavaScriptCore 引擎对标准不完整的日期格式支持较差横杠分隔的日期被判定为非法输入。把横杠换成斜杠之后iOS 和 Android 的行为就一致了。这个函数的另一个好处是同时兼容时间戳和字符串两种输入后端数据结构不统一时前端也不用改业务代码。4.5 坑五TabBar 不显示或图标不生效现象 编译成功但页面底部没有 TabBar或者显示了 TabBar 但图标是空白方格点击后选中态不切换一直停留在第一个页签。原因 最常见的三个原因iconPath指向的文件路径不对图标文件超过了 40KB 限制tabBar.list里的页面路径没有在pages数组里注册。其中“图标文件超 40KB”是最玄学的坑因为它不报任何错误编译也不失败就是图标不显示。解决 逐个排查。用 81x81 像素的 PNG 图标透明背景文件控制在 20KB 以内是安全范围。检查iconPath和selectedIconPath的路径是否精确匹配注意images/目录名不能写错。然后核对tabBar.list里每个pagePath是否都在pages数组中出现过。我后来养成的习惯是修改 TabBar 配置后先清缓存再重新编译微信开发者工具的配置带缓存有时会出现改了不生效的假象。5. 进阶技巧在没有后端的条件下把毕设演示做得游刃有余拿到源码后你可能面临一个很现实的问题后端接口调不通。不管是因为后端代码没写完还是部署的服务器过期了前端页面一进就报错演示没法进行。我的建议是给请求层加一个“模拟数据开关”让前端在没有后端的情况下也能完整跑通所有页面。// utils/request.js const USE_MOCK true // 改成 false 后走真实后端 function request(path, options {}) { if (USE_MOCK) { return mockRequest(path, options) } return realRequest(path, options) } function mockRequest(path) { const mockData { /health/records: require(../mock/health-records.json), /booking/orders: require(../mock/booking-orders.json), /user/profile: require(../mock/user-profile.json) } return Promise.resolve(mockData[path] || {}) }配套在工程根目录建一个mock/文件夹里面按接口路径放预置数据。我建议每个页面准备两套 mock 数据一套包含正常数据另一套是空数组用来测试空状态组件。这样答辩演示时可以主动展示“当前没有预约记录”的空态提示证明你的页面考虑了无数据场景这是导师喜欢看到的细节。答辩时的演示顺序建议这样走先展示首页的老人档案卡片和今日健康概览说明核心设计思路是“一屏掌握老人状态”然后切到健康页按日期分组展示血压记录停在某条异常记录上讲预警逻辑接着进入预约页走一遍发起预约、状态从待接单变为服务中、再到完成的完整链路把ORDER_STATUS状态枚举和filterOrders的映射关系作为重点最后回到首页演示紧急求助按钮调用wx.makePhoneCall直接拉起系统拨号面板这个操作不需要后端参与效果非常直观。有一个我踩过不止一次的教训必须说一下早些年我做毕设时把所有接口地址硬编码在业务代码里答辩当天后端服务崩了演示全程都在转圈加载整场十分钟一个字没讲出来。从那以后我每次拿到一套小程序源码都会强制自己先做一遍“断网关”测试——关掉后端看前端能不能靠 mock 数据把每个页面都跑通。确认可行之后才会把它放进答辩材料里。这个习惯后来救了我很多次也推荐你试试。希望帮到你。本文还有配套的精品资源点击获取
返回列表