S11-12 Vue-项目:mr-vue3-ts-consult-patient
[TOC]
环境搭建
接口文档
地址:https://apifox.com/apidoc/shared-aeb0d03e-c713-4f55-afaf-21cddf542751/api-160608919
技术栈
- Vue3:
@3.5.13
- TS5:
@5.6.3
- Vite6:
@6.0.5
。使用create-vue
创建项目 - Pinia2:
@2.3.0
。 - VueRouter4:
@4.5.0
- Node20:
node@20.11.1
- Vant:
创建项目
使用create-vue
工具创建mr-vue3-ts-consult-patient
项目。create-vue
是基于vite的脚手架工具
pnpm create vue@latest
创建选项
项目配置
Git配置
Eslint配置
1、在.eslintrc.cjs
中配置prettier代码风格
rules: {
'prettier/prettier': [
'warn',
{
singleQuote: true,
semi: false,
printWidth: 80,
trailingComma: 'none',
endOfLine: 'auto'
}
],
// 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
'no-undef': 'error'
}
2、忽略vue组件多单词警告
rules: {
'vue/multi-word-component-names': [
'warn',
{
ignores: ['index']
}
],
}
3、关闭props解构警告
rules: {
'vue/no-setup-props-destructure': ['off'],
}
后面开启响应式语法糖就结构props就不会再丢失响应式。
Husky配置
1、初始化与安装
pnpm dlx husky-init && pnpm install
2、修改 .husky/pre-commit
文件
pnpm lint
lint-staged配置
1、安装
pnpm i lint-staged -D
2、配置 package.json
{
"scripts": {
"lint-staged": "lint-staged"
}
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix"
]
}
}
3、修改 .husky/pre-commit
文件
pnpm lint-staged
目录结构
每一个目录结构的作用:
./src
├── assets `静态资源,图片...`
├── components `通用组件`
├── hook `组合功能通用函数`
├── icon `svg图标`
├── router `路由`
│ └── index.ts
├── service `接口服务API`
├── store `状态仓库`
├── style `样式`
│ └── main.scss
├── type `TS类型`
├── utils `工具函数`
├── views `页面`
├── main.ts `入口文件`
└──App.vue `根组件`
集成-Vant
基本导入
1、安装 vant
pnpm i vant
2、在main.ts
中引入样式
import 'vant/lib/index.css'
3、在组件中使用vant组件
4、推荐按需引入
组件自动注册
已过时
替代方法:使用vant的按需导入
痛点:使用手动导入组件的方法过于繁琐,每次使用时都需要按以下方法手动导入。
解决:使用 unplugin-vue-components 实现自动按需加载,和自动导入组件。
1、安装 unplugin-vue-components 插件
pnpm i unplugin-vue-components -D
2、配置 vite.config.ts
3、优化:样式重复的优化。
问题:配置后会出现vant组件的样式重复。
原因分析:这是因为在main.ts
中导入的vant样式和自动注册组件时导入的vant样式重复了,导入了2次样式。
解决:设置组件自动导入的配置,让自动注册时不要导入样式。
4、优化:类型声明文件重复的优化。(新版待确定)
问题:components.d.ts
的类型声明文件时多余的,vant本身自带了类型声明。
解决:设置组件自动导入的配置,不生成类型声明文件 components.d.ts
。
5、注意:安装了插件后,components
目录下的组件也会自动注册,不需要再手动导入。
移动端适配
pnpm i postcss-px-to-viewport -D
2、配置 postcss.config.js
// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375,
},
},
};
3、重启项目生效
主题定制
使用css变量定制项目主题,和修改vant主题
CSS变量定义/使用
:root {
--main-color: #999; /** 定义CSS全局变量 */
}
.footer {
--footer-color: #f0f; /** 定义CSS局部变量 */
}
a {
color: var(--main-color) /** 使用CSS变量 */
}
项目主题
:root {
// 问诊患者:色板
--cp-primary: #16C2A3;
--cp-plain: #EAF8F6;
--cp-orange: #FCA21C;
--cp-text1: #121826;
--cp-text2: #3C3E42;
--cp-text3: #6F6F6F;
--cp-tag: #848484;
--cp-dark: #979797;
--cp-tip: #C3C3C5;
--cp-disable: #D9DBDE;
--cp-line: #EDEDED;
--cp-bg: #F6F7F9;
--cp-price: #EB5757;
// 覆盖vant主题色
--van-primary-color: var(--cp-primary);
}
集成-Store
useUserStore
TS类型
在type/user.d.ts
中定义User的TS类型
// 用户信息
export type User = {
/** token令牌 */
token: string
/** 用户ID */
id: string
/** 用户名称 */
account: string
/** 手机号 */
mobile: string
/** 头像 */
avatar: string
}
创建Store
1、在store/user.ts
中创建useUserStore
import type { User } from '@/types/user'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('cp-user', () => {
// 用户信息
const user = ref<User>()
// 设置用户,登录后使用
const setUser = (u: User) => {
user.value = u
}
// 清空用户,退出后使用
const delUser = () => {
user.value = undefined
}
return { user, setUser, delUser }
})
2、在组件中设置/删除user
Store持久化
思路:使用 pinia-plugin-persistedstate 实现pinia仓库状态持久化
1、安装 pinia-plugin-persistedstate
插件
pnpm i pinia-plugin-persistedstate
2、在main.ts
中使用插件
3、在 store/user.ts
中配置本地持久化
4、开启后store中的数据会被存储在localStorage中
统一管理
实现仓库统一从 store/index.ts
导出,代码简洁,职能单一,入口唯一
抽取实例
1、抽取pinia实例代码到 store/index.ts
中
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 1. 创建pinia实例
const pinia = createPinia()
// 2. 使用pinia插件
pinia.use(persist)
// 3. 导出pinia实例,给main使用
export default pinia
2、在 main.ts
中挂载pinia实例
import { createApp } from 'vue'
import App from './App.vue'
import pinia from './stores'
import router from './router'
import './styles/main.scss'
const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')
统一导出模块
1、在store/index.ts
中统一导出Store模块
export * from './modules/user'
2、在组件中使用导出的模块
-import { useUserStore } from './stores/user'
+import { useUserStore } from './stores'
集成-axios
1、安装axios
pnpm i axios
2、baseURL,timeout
3、携带token
4、验证携带token
响应成功,业务失败处理
业务失败:响应数据返回的code不是10000(项目后端自定义的规则)。
失败处理:
- 弹出轻提示
- 此时返回一个失败的promise,传递code给catch,以便后续根据code进行不同的处理
1、在axios响应拦截器中处理业务失败
2、测试:登录失败
返回核心数据
需求:业务逻辑成功,返回响应数据data,后续直接使用
代码实现:
401错误处理
401错误:token失效。
401错误处理:
- 删除用户信息
- 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
// 3. 响应拦截器,剥离无效数据,401拦截
instance.interceptors.response.use(
(res) => {
// 后台约定,响应成功,但是code不是10000,是业务逻辑失败
if (res.data?.code !== 10000) {
showToast(res.data?.message || '业务失败')
return Promise.reject(res.data)
}
// 业务逻辑成功,返回响应数据,作为axios成功的结果
return res.data
},
(err) => {
if (err.response.status === 401) {
// 1. 删除用户信息
const store = useUserStore()
store.delUser()
// 2. 跳转登录,带上接口失效所在页面的地址,登录完成后回跳使用
router.push({
path: '/login',
query: { returnUrl: router.currentRoute.value.fullPath }
})
}
return Promise.reject(err)
}
)
封装请求函数
封装一个统一的请求函数,简化请求配置
测试
设置响应数据类型
问题:为什么T传递给了instance.request<any, T>
的第二个参数。
解释:第一个参数是给res设置的类型,但是在响应拦截器中返回的是res.data
的数据,如果想给它设置类型,只能通过第二个参数。
打包svg地图@
Login
路由规则
1、在router/index.ts
中配置路由匹配规则
2、在App.vue
中配置一级路由占位
组件:cp-nav-bar
页面布局
1、vant相关属性
2、在login/index.vue
中使用组件
3、实现 <cp-nav-bar>
组件
4、修改样式
注意:使用 :deep()
修改vant组件内部样式。
功能
动态标题、右侧文字
思路:通过props来动态设置标题、右侧文字。
1、在子组件中,通过defineProps()接收传递的属性 title、rightText。
2、在父组件中,使用<cp-nav-bar>
组件时绑定属性 title、rightText。
右侧文字点击事件
思路:通过emit方法触发自定义右侧文字点击事件。
1、在子组件中,监听vant组件的@click-right
事件,同时自定义@click-right
事件向外发射该事件。
2、在父组件中,监听子组件传递过来的自定义事件@click-right
。
返回功能
思路:通过 history.state
访问历史记录信息。
1、在子组件中,监听@click-left
事件
2、判断history.state.back
是否有值,实现回退到不同页面。
组件类型提示@
问题:当前的组件<cp-nav-bar>
是没有类型的:
解决:
思路一:在使用时,显示(手动)导入组件。
思路二:对于全局组件或自动注册的组件,可以在
components.d.ts
文件中添加全局组件类型。
组件:login
页面布局
1、在style/main.scss
中全局重置样式
// 全局样式
body {
font-size: 14px;
color: var(--cp-text1);
}
a {
color: var(--cp-text2);
}
h1,h2,h3,h4,h5,h6,p,ul,ol {
margin: 0;
padding: 0;
}
2、页面结构
<script setup lang="ts"></script>
<template>
<div class="login-page">
<!-- 导航栏 -->
<cp-nav-bar
right-text="注册"
@click-right="$router.push('/register')"
></cp-nav-bar>
<!-- 头部 -->
<div class="login-head">
<h3>密码登录</h3>
<a href="javascript:;">
<span>短信验证码登录</span>
<van-icon name="arrow"></van-icon>
</a>
</div>
<!-- 表单 -->
<van-form autocomplete="off">
<van-field placeholder="请输入手机号" type="tel"></van-field>
<van-field placeholder="请输入密码" type="password"></van-field>
<div class="cp-cell">
<van-checkbox>
<span>我已同意</span>
<a href="javascript:;">用户协议</a>
<span>及</span>
<a href="javascript:;">隐私条款</a>
</van-checkbox>
</div>
<div class="cp-cell">
<van-button block round type="primary">登 录</van-button>
</div>
<div class="cp-cell">
<a href="javascript:;">忘记密码?</a>
</div>
</van-form>
<!-- 底部 -->
<div class="login-other">
<van-divider>第三方登录</van-divider>
<div class="icon">
<img src="@/assets/qq.svg" alt="" />
</div>
</div>
</div>
</template>
3、样式
.login {
&-page {
padding-top: 46px;
}
&-head {
display: flex;
padding: 30px 30px 50px;
justify-content: space-between;
align-items: flex-end;
line-height: 1;
h3 {
font-weight: normal;
font-size: 24px;
}
a {
font-size: 15px;
}
}
&-other {
margin-top: 60px;
padding: 0 30px;
.icon {
display: flex;
justify-content: center;
img {
width: 36px;
height: 36px;
padding: 4px;
}
}
}
}
.van-form {
padding: 0 14px;
.cp-cell {
height: 52px;
line-height: 24px;
padding: 14px 16px;
box-sizing: border-box;
display: flex;
align-items: center;
.van-checkbox {
a {
color: var(--cp-primary);
padding: 0 5px;
}
}
}
.btn-send {
color: var(--cp-primary);
&.active {
color: rgba(22,194,163,0.5);
}
}
}
4、在style/main.scss
中修改样式
// 覆盖vant主体色
--van-primary-color: var(--cp-primary);
// 单元格上下间距
--van-cell-vertical-padding: 14px;
// 复选框大小
--van-checkbox-size: 14px;
// 默认按钮文字大小
--van-button-normal-font-size: 16px;
5、清除多余app容器
功能
表单校验
提取校验规则@
1、提取校验规则到utils/rule.ts
文件。
2、设置校验规则的TS类型
基本校验
1、校验手机号
2、校验密码
3、表单整体校验,修改native-type
属性
校验勾选协议
1、绑定 agree ,判断是否勾选协议
2、在submit事件处理方法中,校验是否勾选协议
密码登录
接口
URL:
/login/password
类型:
POST
参数:
ts{ password: string, // 密码 mobile: string // 手机号 }
返回数据:
密码登录
1、在 service/user.ts 中发送网络请求
2、在 login/login.vue 中执行密码登录
短信登录
切换界面
1、根据 isPass 切换密码登录和短信验证码登录界面
2、表单项切换
3、校验验证码
获取验证码-接口
URL:/code
类型:GET
token:携带
参数:
{
mobile: string, // 手机号
type: 'login' | 'register' | 'changeMobile' | 'forgetPassword' | 'bindMobile'
}
返回数据:
获取验证码
1、发送前校验
2、在 service/user.ts 中发送网络请求
3、在types/user.ts
中定义CodeType联合类型
4、在login
组件中,发送请求获取验证码,并设置倒计时
5、实现倒计时,并在结束时清理定时器
6、在组件销毁时清理定时器
7、显示倒计时
短信登录-接口
URL:/login
类型:POST
token:携带
参数:
{
code: string, // 验证码
mobile: string // 手机号
}
返回数据:
短信登录
1、在 service/user.ts 中发送网络请求
2、在组件中将短信登录合并到密码登录逻辑中
密码是否可见
Layout
路由规则
1、在router/index.ts
中配置路由匹配规则
2、在views/layout/layout.vue
中配置二级路由占位
组件:van-tabbar
1、基本使用
2、开启路由模式
3、自定义图标
4、修改样式
.layout-page {
:deep() {
.van-tabbar-item {
&__icon {
font-size: 21px;
}
&__text {
font-size: 11px;
}
&:not(.van-tabbar-item--active) {
color: var(--cp-text3);
}
}
}
}
功能
访问权限控制
router.beforeEach():(guard)
,全局前置导航守卫,用于在每次路由跳转前执行自定义逻辑,如权限校验、数据预加载等。
思路:在 router/index.ts 中添加全局前置导航守卫,校验是否有token、是否在白名单中。
白名单:不需要登录就可以访问的页面
页面标题
router.afterEach():(guard)
,全局后置守卫,用于注册一个在 导航完成之后 执行的钩子函数。它不会改变导航结果,常用于执行与导航结果无关的后续处理操作,如埋点统计、页面标题更新等。
1、在路由配置元信息 meta 中定义标题
2、在 router/index.ts 中添加全局后置导航守卫,获取 meta 并设置标题
3、TS:如果需要TS提示title,可以通过扩展元信息类型实现
解决:新建 types/vue-router.d.ts 文件,并扩展元信息类型
加载进度
依赖包:nprogress
1、安装 nprogress和@types/nprogress
pnpm i nprogress
pnpm i @types/nprogress -D # TS类型提示
2、在 router/index.ts 中导入并在前置守卫中开启
3、在 router/index.ts 中的后置守卫中结束
4、取消进度条的小圆圈动画
5、在 style/main.less 中修改进度条样式
User
组件:user-info
TS类型
知识点:Omit
知识点:Pick
// 用户信息
export type User = {
token: string
id: string
account: string
mobile: string
avatar: string
}
// 个人信息
type OmitUser = Omit<User, 'token'>
export type UserInfo = OmitUser & {
/** 关注 */
likeNumber: number
/** 收藏 */
collectionNumber: number
/** 积分 */
score: number
/** 优惠券 */
couponNumber: number
orderInfo: {
/** 待付款 */
paidNumber: number
/** 待发货 */
receivedNumber: number
/** 待收货 */
shippedNumber: number
/** 已完成 */
finishedNumber: number
}
}
页面布局
<script setup lang="ts"></script>
<template>
<div class="user-page">
<div class="user-page-head">
<!-- 头部 -->
<div class="top">
<van-image
round
fit="cover"
src="https://yanxuan-item.nosdn.127.net/ef302fbf967ea8f439209bd747738aba.png"
/>
<div class="name">
<p>用户907456</p>
<p><van-icon name="edit" /></p>
</div>
</div>
<!-- 用户信息 -->
<van-row>
<van-col span="6">
<p>150</p>
<p>收藏</p>
</van-col>
<van-col span="6">
<p>23</p>
<p>关注</p>
</van-col>
<van-col span="6">
<p>270</p>
<p>积分</p>
</van-col>
<van-col span="6">
<p>3</p>
<p>优惠券</p>
</van-col>
</van-row>
</div>
<!-- 药品订单 -->
<div class="user-page-order">
<div class="head">
<h3>药品订单</h3>
<router-link to="/order">全部订单 <van-icon name="arrow" /></router-link>
</div>
<van-row>
<van-col span="6">
<cp-icon name="user-paid" />
<p>待付款</p>
</van-col>
<van-col span="6">
<cp-icon name="user-shipped" />
<p>待发货</p>
</van-col>
<van-col span="6">
<cp-icon name="user-received" />
<p>待收货</p>
</van-col>
<van-col span="6">
<cp-icon name="user-finished" />
<p>已完成</p>
</van-col>
</van-row>
</div>
</div>
</template>
2、样式
.user-page {
background-color: var(--cp-bg);
min-height: calc(100vh - 50px);
padding: 0 15px 65px;
// 头部
&-head {
height: 200px;
background: linear-gradient(180deg, rgba(44, 181, 165, 0.46), rgba(44, 181, 165, 0));
margin: 0 -15px;
padding: 0 15px;
.top {
display: flex;
padding-top: 50px;
align-items: center;
.van-image {
width: 70px;
height: 70px;
}
.name {
padding-left: 10px;
p {
&:first-child {
font-size: 18px;
font-weight: 500;
}
&:last-child {
margin-top: 10px;
color: var(--cp-primary);
font-size: 16px;
}
}
}
}
.van-row {
margin: 0 -15px;
padding-top: 15px;
p {
text-align: center;
&:first-child {
font-size: 18px;
font-weight: 500;
}
&:last-child {
color: var(--cp-dark);
font-size: 12px;
padding-top: 4px;
}
}
}
}
// 订单
&-order {
background-color: #fff;
border-radius: 8px;
margin-bottom: 15px;
padding-bottom: 15px;
.head {
display: flex;
justify-content: space-between;
line-height: 50px;
padding: 0 15px;
a {
color: var(--cp-tip);
}
}
.van-col {
text-align: center;
.cp-icon {
font-size: 28px;
}
p {
font-size: 12px;
padding-top: 4px;
}
}
}
// 分组
&-group {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
h3 {
padding-left: 16px;
line-height: 44px;
}
.van-cell {
align-items: center;
}
.cp-icon {
font-size: 17px;
margin-right: 10px;
}
}
.logout {
display: block;
margin: 20px auto;
width: 100px;
text-align: center;
color: var(--cp-price);
}
}
接口
URL:
/patient/myUser
类型:
GET
token:携带
参数:无
返回数据:
渲染页面
1、在 service/user.ts 中发送网络请求
2、在组件中调用网路请求方法
2、在组件中渲染请求的数据
组件:user-quick-toolbar
页面布局
1、快捷工具数据
2、遍历渲染
3、退出登录
功能
退出登录
1、绑定点击事件
2、实现点击事件处理函数
Patient
路由规则
组件:patient-list
页面布局
1、HTML
<script setup lang="ts"></script>
<template>
<div class="patient-page">
<!-- 导航栏 -->
<cp-nav-bar title="家庭档案"></cp-nav-bar>
<!-- 患者列表 -->
<div class="patient-list">
<div class="patient-item">
<div class="info">
<span class="name">李富贵</span>
<span class="id">321111********6164</span>
<span>男</span>
<span>32岁</span>
</div>
<div class="icon"><cp-icon name="user-edit" /></div>
<div class="tag">默认</div>
</div>
<div class="patient-item">
<div class="info">
<span class="name">李富贵</span>
<span class="id">321333********6164</span>
<span>男</span>
<span>32岁</span>
</div>
<div class="icon"><cp-icon name="user-edit" /></div>
</div>
<!-- 添加患者 -->
<div class="patient-add">
<cp-icon name="user-add" />
<p>添加患者</p>
</div>
<!-- 提示 -->
<div class="patient-tip">最多可添加 6 人</div>
</div>
</div>
</template>
2、样式
.patient-page {
padding: 46px 0 80px;
}
.patient-list {
padding: 15px;
}
.patient-item {
display: flex;
align-items: center;
padding: 15px;
background-color: var(--cp-bg);
border-radius: 8px;
margin-bottom: 15px;
position: relative;
border: 1px solid var(--cp-bg);
transition: all 0.3s;
overflow: hidden;
.info {
display: flex;
flex-wrap: wrap;
flex: 1;
span {
color: var(--cp-tip);
margin-right: 20px;
line-height: 30px;
&.name {
font-size: 16px;
color: var(--cp-text1);
width: 80px;
margin-right: 0;
}
&.id {
color: var(--cp-text2);
width: 180px;
}
}
}
.icon {
color: var(--cp-tag);
width: 20px;
text-align: center;
}
.tag {
position: absolute;
right: 60px;
top: 21px;
width: 30px;
height: 16px;
font-size: 10px;
color: #fff;
background-color: var(--cp-primary);
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
}
&.selected {
border-color: var(--cp-primary);
background-color: var(--cp-plain);
.icon {
color: var(--cp-primary);
}
}
}
.patient-add {
background-color: var(--cp-bg);
color: var(--cp-primary);
text-align: center;
padding: 15px 0;
border-radius: 8px;
.cp-icon {
font-size: 24px;
}
}
.patient-tip {
color: var(--cp-tag);
padding: 12px 0;
}
.pb4 {
padding-bottom: 4px;
}
接口
URL:
/patient/mylist
类型:
GET
token:携带
参数:无
返回数据:
TS类型
// 家庭档案-患者信息
export type Patient = {
/** 患者ID */
id: string
/** 患者名称 */
name: string
/** 身份证号 */
idCard: string
/** 0不默认 1默认 */
defaultFlag: 0 | 1
/** 0 女 1 男 */
gender: 0 | 1
/** 性别文字 */
genderValue: string
/** 年龄 */
age: number
}
// 家庭档案-患者信息列表
export type PatientList = Patient[]
渲染页面
1、在 services/user.ts 中发送网络请求
2、在组件中,调用请求方法,获取数据
3、渲染数据
功能
身份证脱敏@
知识点:通过$1
、$2
可以获取正则匹配到的内容。
添加患者
组件:cp-radio-btn
页面布局
1、HTML
<script setup lang="ts"></script>
<template>
<div class="cp-radio-btn">
<a class="item" href="javascript:;">男</a>
<a class="item" href="javascript:;">女</a>
</div>
</template>
2、样式
.cp-radio-btn {
display: flex;
flex-wrap: wrap;
.item {
height: 32px;
min-width: 60px;
line-height: 30px;
padding: 0 14px;
text-align: center;
border: 1px solid var(--cp-bg);
background-color: var(--cp-bg);
margin-right: 10px;
box-sizing: border-box;
color: var(--cp-text2);
margin-bottom: 10px;
border-radius: 4px;
transition: all 0.3s;
&.active {
border-color: var(--cp-primary);
background-color: var(--cp-plain);
}
}
}
动态渲染选项
1、定义选项数据
2、使用组件,并传入数据
3、接收并遍历渲染数据
功能
切换选中项
1、在父组件中定义 gender 属性,并绑定到 modelValue上
2、在子组件中接收 gender 属性,并根据gender值设置active样式
3、绑定点击事件,向外发射自定义事件 @update:modelValue
4、在父组件中绑定子组件传递的自定义事件 @update:modelValue
5、重构:使用v-model重构
显示弹层
1、在 patient-list组件中,使用 van-popup 组件添加弹层
2、点击 添加患者 按钮,展示弹层
3、修改弹层样式
.patient-page {
padding: 46px 0 80px;
:deep() {
.van-popup {
width: 80%;
height: 100%;
}
}
}
组件:patient-add
页面布局
<van-form autocomplete="off" ref="form">
<van-field label="真实姓名" placeholder="请输入真实姓名" />
<van-field label="身份证号" placeholder="请输入身份证号" />
<van-field label="性别" class="pb4">
<!-- 单选按钮组件 -->
<template #input>
<cp-radio-btn :options="options"></cp-radio-btn>
</template>
</van-field>
<van-field label="默认就诊人">
<template #input>
<van-checkbox :icon-size="18" round />
</template>
</van-field>
</van-form>
接口-添加患者
URL:
/patient/add
类型:
POST
token:携带
参数:
ts{ name: string // 患者姓名 idCard: string // 患者身份证号 defaultFlag: number // 是否设置为默认患者 gender: number // 性别,1:男,0:女 }
返回数据:
TS类型修改
问题:patient表单只需要4个属性,而Patient类型有7个属性,因此需要将其他属性修改为可选属性。
解决:修改Patient类型为可选属性。
渲染表单
功能
重构cp-nav-bar
1、在 cp-nav-bar 组件中定义 back 属性,类型是一个回调函数
2、重构 onClickLeft 方法,如果传入了back,则执行该回调,而不是之前的逻辑
3、在父组件中,传入back属性,实现自定义的关闭弹层逻辑
默认就诊人类型转换
问题:默认就诊人给的数据是 0 和 1,而 van-checkbox 值的类型为 true 和 false,需要通过计算属性转换后才能使用。
解决:通过计算属性转换后使用。
重置表单
需求:每次打开侧滑弹层时,需要将上次弹层中的表单数据清空。
表单校验
1、表单项校验
2、提交时校验整个表单
3、性别确认提示
思路:身份证号倒数第二位,如果是偶数就是女,如果是奇数就是男。
实现添加患者
1、在 service/user.ts 中发送网络请求
2、在组件中调用请求方法,实现添加患者
3、注意:如果添加的身份证号已经存在于患者列表中会提示添加失败
编辑患者
思路:编辑患者和添加患者共用一个组件。
接口
URL:
/patient/update
类型:
PUT
token:携带
参数:
ts{ name: string // 患者姓名 idCard: string // 患者身份证号 defaultFlag: number // 是否设置为默认患者 gender: number // 性别,1:男,0:女 id: string // 患者信息id }
返回数据:
功能
显示弹层
区分标题
思路:根据 patient 对象是否存在id属性,判断是否是编辑状态,显示不同的标题
实现编辑患者
1、在 service/user.ts 中发送网络请求
2、在组件中调用请求方法,将编辑患者逻辑合并到添加患者中
清空校验
需求:当再次打开弹层时,上次的校验结果依然存在,需要清空。
删除患者
接口
URL:
/patient/del/{id}
类型:
DELETE
token:携带
参数:
tsid: string // 患者信息id
返回数据:
功能
删除按钮
需求:在弹层的底部添加一个删除按钮
修改样式
// 底部操作栏
.van-action-bar {
padding: 0 10px;
margin-bottom: 10px;
.van-button {
color: var(--cp-price);
background-color: var(--cp-bg);
}
}
实现删除患者
1、在 service/user.ts 中发送网络请求
2、在组件中调用请求方法,实现删除患者
Home
页面布局
1、HTML
<script setup lang="ts"></script>
<template>
<div class="home-page">
<!-- 头部 -->
<div class="home-header">
<div class="con">
<h1>优医</h1>
<div class="search">
<cp-icon name="home-search" /> 搜一搜:疾病/症状/医生/健康知识
</div>
</div>
</div>
<!-- 导航 -->
<div class="home-navs">
<van-row>
<van-col span="8">
<router-link to="/" class="nav">
<cp-icon name="home-doctor"></cp-icon>
<p class="title">问医生</p>
<p class="desc">按科室查问医生</p>
</router-link>
</van-col>
<van-col span="8">
<router-link to="/consult/fast" class="nav">
<cp-icon name="home-graphic"></cp-icon>
<p class="title">极速问诊</p>
<p class="desc">20s医生极速回复</p>
</router-link>
</van-col>
<van-col span="8">
<router-link to="/" class="nav">
<cp-icon name="home-prescribe"></cp-icon>
<p class="title">开药门诊</p>
<p class="desc">线上买药更方便</p>
</router-link>
</van-col>
</van-row>
<van-row>
<van-col span="6">
<router-link to="/" class="nav min">
<cp-icon name="home-order"></cp-icon>
<p class="title">药品订单</p>
</router-link>
</van-col>
<van-col span="6">
<router-link to="/" class="nav min">
<cp-icon name="home-docs"></cp-icon>
<p class="title">健康档案</p>
</router-link>
</van-col>
<van-col span="6">
<router-link to="/" class="nav min">
<cp-icon name="home-rp"></cp-icon>
<p class="title">我的处方</p>
</router-link>
</van-col>
<van-col span="6">
<router-link to="/" class="nav min">
<cp-icon name="home-find"></cp-icon>
<p class="title">疾病查询</p>
</router-link>
</van-col>
</van-row>
</div>
<!-- 轮播图 -->
<div class="home-banner">
<van-swipe indicator-color="#fff">
<van-swipe-item>
<img src="@/assets/ad.png" alt="" />
</van-swipe-item>
<van-swipe-item>
<img src="@/assets/ad.png" alt="" />
</van-swipe-item>
</van-swipe>
</div>
<!-- 知识列表tab -->
<van-tabs shrink sticky v-model:active="active">
<van-tab title="关注">1</van-tab>
<van-tab title="推荐" >
<p v-for="i in 100" :key="i">内容</p>
</van-tab>
<van-tab title="减脂">3</van-tab>
<van-tab title="饮食">4</van-tab>
</van-tabs>
</div>
</template>
2、样式
.home-page {
padding-bottom: 50px;
}
.home-header {
height: 100px;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 90px;
background: linear-gradient(180deg, rgba(62, 206, 197, 0.85), #26bcc6);
border-bottom-left-radius: 150px 20px;
border-bottom-right-radius: 150px 20px;
}
.con {
position: relative;
padding: 0 15px;
> h1 {
font-size: 18px;
color: #fff;
font-weight: normal;
padding: 20px 0;
line-height: 1;
padding-left: 5px;
}
.search {
height: 40px;
border-radius: 20px;
box-shadow: 0px 15px 22px -7px rgba(224, 236, 250, 0.8);
background-color: #fff;
display: flex;
align-items: center;
padding: 0 20px;
color: var(--cp-dark);
font-size: 13px;
.cp-icon {
font-size: 16px;
margin-right: 5px;
}
}
}
}
.home-navs {
padding: 10px 15px 0 15px;
.nav {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
.cp-icon {
font-size: 48px;
}
.title {
font-weight: 500;
margin-top: 5px;
color: var(--cp-text1);
}
.desc {
font-size: 11px;
color: var(--cp-tag);
margin-top: 2px;
}
&.min {
.cp-icon {
font-size: 31px;
}
.title {
font-size: 13px;
color: var(--cp-text2);
font-weight: normal;
}
}
}
}
.home-banner {
padding: 10px 15px;
height: 100px;
img {
width: 100%;
height: 100%;
}
}
3、全局覆盖van-tab样式
// 全局覆盖van-tab样式
.van-tabs {
.van-tabs__nav {
padding: 0 0 15px 0;
}
.van-tabs__line {
width: 20px;
background-color: var(--cp-primary);
}
.van-tab {
padding: 0 15px;
}
}
组件:knowledge-card
页面布局
1、HTML
<template>
<div class="knowledge-card van-hairline--bottom">
<div class="head">
<van-image
round
class="avatar"
src="https://yanxuan-item.nosdn.127.net/9ad83e8d9670b10a19b30596327cfd14.png"
></van-image>
<div class="info">
<p class="name">张医生</p>
<p class="dep van-ellipsis">积水潭医院 骨科 主任医师</p>
</div>
<van-button class="btn" size="small" round>+ 关注</van-button>
</div>
<div class="body">
<h3 class="title van-ellipsis">高血压是目前世界上最常见,发病率最高的慢性病之一</h3>
<p class="tag">
<span># 肥胖</span>
<span># 养生</span>
</p>
<p class="intro van-multi-ellipsis--l2">
据估计,全世界有 10
亿人患有高血压,来自美国全国健康和营养调查的数据(NHANES)显示,高血压的患病率呈逐年上升趋势。
但是,我国高血压的控制程度非常不乐观,不少朋友担心降压药对肾的影响,有些甚至因为担心伤肾,而不敢吃降压药。
我们就介绍一下,高血压对肾脏的危害,还有降压药对肾脏影响。
没有耐心看的朋友,可以直接记住这个结论:高血压比降压药伤肾。千万不要因为担心副作用不敢吃药,那是「丢西瓜捡芝麻」得不偿失的行为
</p>
<div class="imgs">
<van-image
src="https://yanxuan-item.nosdn.127.net/c1cdf62c5908659a9e4c8c2f9df218fd.png"
/>
<van-image
src="https://yanxuan-item.nosdn.127.net/c1cdf62c5908659a9e4c8c2f9df218fd.png"
/>
<van-image
src="https://yanxuan-item.nosdn.127.net/c1cdf62c5908659a9e4c8c2f9df218fd.png"
/>
</div>
<p class="logs">
<span>10 收藏</span>
<span>50 评论</span>
</p>
</div>
</div>
</template>
2、样式
.knowledge-card {
padding: 20px 0 16px;
.head {
display: flex;
align-items: center;
.avatar {
width: 38px;
height: 38px;
margin-right: 10px;
}
.info {
width: 200px;
padding-right: 10px;
.name {
color: var(--cp-text2);
}
.dep {
color: var(--cp-tip);
font-size: 12px;
}
}
.btn {
padding: 0 12px;
border-color: var(--cp-primary);
color: var(--cp-primary);
height: 28px;
width: 72px;
}
}
.body {
.title {
font-size: 16px;
margin-top: 8px;
font-weight: normal;
}
.tag {
margin-top: 6px;
> span {
color: var(--cp-primary);
margin-right: 20px;
font-size: 12px;
}
}
.intro {
margin-top: 7px;
line-height: 2;
color: var(--cp-text3);
}
.imgs {
margin-top: 7px;
display: flex;
.van-image {
width: 106px;
height: 106px;
margin-right: 12px;
border-radius: 12px;
overflow: hidden;
&:last-child {
margin-right: 0;
}
}
&.large {
.van-image {
width: 185px;
height: 125px;
}
}
}
.logs {
margin-top: 10px;
> span {
color: var(--cp-tip);
margin-right: 16px;
font-size: 12px;
}
}
}
}
组件:knowledge-list
页面布局
1、HTML
<template>
<div class="knowledge-list">
<knowledge-card v-for="i in 5" :key="i"></knowledge-card>
</div>
</template>
2、样式
.knowledge-list {
padding: 0 15px;
}
3、在Home组件中使用 knowlege-list
<van-tabs shrink sticky v-model:active="active">
<van-tab title="关注"><knowledge-list /></van-tab>
<van-tab title="推荐"><knowledge-list /></van-tab>
<van-tab title="减脂"><knowledge-list /></van-tab>
<van-tab title="饮食"><knowledge-list /></van-tab>
</van-tabs>
功能
列表加载更多
1、使用 van-list 实现列表加载更多功能
2、模拟加载后台数据
3、遍历加载的数据
渲染请求数据
TS类型
1、响应数据类型
// 文章信息类型
export type Knowledge = {
id: string
/** 标题 */
title: string
/** 封面[] */
coverUrl: string[]
/** 标签[] */
topics: string[]
/** 收藏数 */
collectionNumber: number
/** 评论数 */
commentNumber: number
/** 医生名称 */
creatorName: string
/** 医生头像 */
creatorAvatar: string
/** 医生医院 */
creatorHospatalName: string
/** 关注文章 */
likeFlag: 0 | 1
/** 内容 */
content: string
/** 医生科室 */
creatorDep: string
/** 医生职称 */
creatorTitles: string
/** 医生ID */
creatorId: string
}
// 文章列表
export type KnowledgeList = Knowledge[]
// 文章列表带分页
export type KnowledgePage = {
pageTotal: number
total: number
rows: KnowledgeList
}
2、查询参数类型
// props类型 recommend推荐,fatReduction减脂,food健康饮食,like关注医生页面文章
export type KnowledgeType = 'like' | 'recommend' | 'fatReduction' | 'food'
// 文章列表查询参数
export type KnowledgeParams = {
type: KnowledgeType
current: number
pageSize: number
}
知识列表类型
1、在 kownlege-list 组件中,定义props类型
2、在使用组件时添加type属性
接口
URL:
/patient/home/knowledge
类型:
GET
token:携带
参数:
ts{ type: KnowledgeType // recommend: 推荐,fatReduction: 减脂,food: 健康饮食,like: 关注医生页面文章 current: number pageSize: number }
返回数据:
渲染数据
1、在 services/home.ts 中发送网络请求
2、在组件中调用请求方法,获取知识列表数据
3、遍历并传递数据到 kownlege-card 中
4、渲染数据到 kownlege-card 中
5、修复:图片变形
6、修复:去除HTML代码
7、修复:一张配图展示
组件:doctor-card
页面布局
1、HTML
<template>
<div class="doctor-card">
<van-image
round
src="https://yanxuan-item.nosdn.127.net/3cb61b3fd4761555e56c4a5f19d1b4b1.png"
/>
<p class="name">周医生</p>
<p class="van-ellipsis">积水潭医院 神经内科</p>
<p>副主任医师</p>
<van-button round size="small" type="primary">+ 关注</van-button>
</div>
</template>
2、样式
.doctor-card {
width: 135px;
height: 190px;
background: #fff;
border-radius: 20px;
box-shadow: 0px 0px 11px 0px rgba(229, 229, 229, 0.2);
text-align: center;
padding: 15px;
margin-left: 15px;
display: inline-block;
box-sizing: border-box;
> .van-image {
width: 58px;
height: 58px;
vertical-align: top;
border-radius: 50%;
margin: 0 auto 8px;
}
> p {
margin-bottom: 0;
font-size: 11px;
color: var(--cp-tip);
&.name {
font-size: 13px;
color: var(--cp-text1);
margin-bottom: 5px;
}
}
> .van-button {
padding: 0 12px;
height: 28px;
margin-top: 8px;
width: 72px;
}
}
组件:follow-doctor
页面布局
1、使用组件
1、HTML
<template>
<div class="follow-doctor">
<div className="head">
<p>推荐关注</p>
<a href="javascript:;"> 查看更多<i class="van-icon van-icon-arrow" /></a>
</div>
<div class="body">
<!-- swipe 组件 -->
</div>
</div>
</template>
2、使用 van-swipe 组件遍历包裹 doctor-card 组件
3、样式
.follow-doctor {
background-color: var(--cp-bg);
height: 250px;
.head {
display: flex;
justify-content: space-between;
height: 45px;
align-items: center;
padding: 0 15px;
font-size: 13px;
> a {
color: var(--cp-tip);
}
}
.body {
width: 100%;
overflow: hidden;
}
}
渲染请求数据
TS类型
// 医生卡片对象
export type Doctor = {
/** 医生ID */
id: string
/** 医生名称 */
name: string
/** 头像 */
avatar: string
/** 医院名称 */
hospitalName: string
/** 医院等级 */
gradeName: string
/** 科室 */
depName: string
/** 职称 */
positionalTitles: string
/** 是否关注,0 未关注 1 已关注 */
likeFlag: 0 | 1
/** 接诊服务费 */
serviceFee: number
/** 接诊人数 */
consultationNum: number
/** 评分 */
score: number
/** 主攻方向 */
major: string
}
接口
URL:
/home/page/doc
类型:
GET
token:携带
参数:
ts{ current: number pageSize: number }
返回数据:
渲染数据
1、在 services/home.ts 中发送网络请求
2、在组件中调用请求方法,获取医生列表数据
3、遍历并传递数据到 doctor-card 中
4、渲染数据到 doctor-card 中
功能
调整卡片间距
需求:需要调整 van-swipe-item 宽度,让卡片更加紧凑
实现:
清除无限滚动
清除指示器
适配滑动宽度@
依赖包:vueuse
安装:
pnpm i @vueuse/core
宽度公式: 375 / 150 = 设备宽度 / x
,设备宽度可以通过 useWindowSize() 响应式获取。
实现:
1、通过 vueuse 的 useWindowSize() 方法响应式获取设备宽度
2、响应式设置 van-swipe 的宽度
扩展:使用原生的方式实现
关注医生
接口
URL:
/like
类型:
POST
token:携带
参数:
ts{ type: string // topic: 百科话题, knowledge: 百科文章, doc: 医生, disease: 疾病 id: string // 对应的id }
返回数据:
TS类型
实现关注
1、在 services/home.ts 中发送网络请求
2、在组件中,当点击关注/取消关注按钮时,调用请求方法实现关注/取消关注功能
关注文章
封装关注逻辑@
1、将关注逻辑封装到 hooks/index.ts 中
2、在 doctor-card 组件中导入并使用封装的hook
实现关注文章
1、修改封装的 useFollow() 方法,添加type参数,修改item类型
2、在 knowledge-card 组件中,使用 useFollow() 方法,实现关注文章
Consult
TS类型
知识点:Enum
1、使用枚举类型定义问诊类型、问诊时间
// 问诊类型
export enum ConsultType {
/** 找医生 */
Doctor = 1,
/** 快速问诊 */
Fast = 2,
/** 开药问诊 */
Medication = 3
}
// 问诊时间,以1自增可以省略
export enum IllnessTime {
/** 一周内 */
Week = 1,
/** 一月内 */
Month,
/** 半年内 */
HalfYear,
/** 半年以上 */
More
}
2、定义问诊记录类型
import { ConsultType, IllnessTime } from '@/enums'
// 图片列表
export type Image = {
/** 图片ID */
id: string
/** 图片地址 */
url: string
}
// 问诊记录
export type Consult = {
/** 问诊记录ID */
id: string
/** 问诊类型 */
type: ConsultType
/** 快速问诊类型,0 普通 1 三甲 */
illnessType: 0 | 1
/** 科室ID */
depId: string
/** 疾病描述 */
illnessDesc: string
/** 疾病持续时间 */
illnessTime: IllnessTime
/** 是否就诊过,0 未就诊过 1 就诊过 */
consultFlag: 0 | 1
/** 图片数组 */
pictures: Image[]
/** 患者ID */
patientId: string
/** 优惠券ID */
couponId: string
}
// 问诊记录-全部可选
export type PartialConsult = Partial<Consult>
// Required 转换为全部必须 Partial 转换问全部可选 两个内置的泛型类型