Appearance
开药门诊模块
开药门诊-需求分析
理解开药门诊阶段流程分析

开药门诊-表单页面路由搭建
步骤
- 准备基础结构代码
- 路由配置
- 首页跳转
- cv静态结构
基础结构代码views/Consult/ConsultMedicine.vue
vue
<script setup lang="ts"></script>
<template>
<div class="consult-medicine-page">
<h1>consult-medicine</h1>
</div>
</template>
<style scoped lang="scss"></style>路由配置router/index.ts
ts
{
path: '/consult/medicine',
component: () => import('@/views/Consult/ConsultMedicine.vue'),
meta: { title: '开药门诊' }
}首页跳转Home/index.vue
vue
<router-link to="/consult/medicine" class="nav" @click="store.setType(ConsultType.Medication)">
<cp-icon name="home-prescribe"></cp-icon>
<p class="title">开药门诊</p>
<p class="desc">线上买药更方便</p>
</router-link>静态结构ConsultMedicine.vue
vue
<script setup lang="ts"></script>
<template>
<div class="consult-medicine-page">
<cp-nav-bar title="开药门诊" right-text="问诊记录" @click-right="$router.push('/user/consult')"></cp-nav-bar>
<van-notice-bar text="请如实填写资料以便医生了解您的病情和用药需求" />
<div class="illness-form">
<div class="adm-list-header">症状描述</div>
<van-field type="textarea" rows="3" placeholder="请输入所患疾病名称"></van-field>
<div class="adm-list-header">用药人身体情况</div>
<div class="item">
<p>肝功能</p>
</div>
<div class="item">
<p>肾功能</p>
</div>
<div class="item">
<p>过敏史</p>
</div>
<div class="item">
<p>生育状态及计划</p>
</div>
<div class="adm-list-header">补充病例信息</div>
<!-- 上传组件 -->
<div class="illness-img"></div>
<!-- 下一步 -->
<!-- <van-button type="primary" block round> 下一步 </van-button> -->
</div>
</div>
</template>
<style scoped lang="scss">
.consult-medicine-page {
padding-top: 46px;
.van-button {
font-size: 16px;
margin-bottom: 30px;
&.disabled {
opacity: 1;
background: #fafafa;
color: #d9dbde;
border: #fafafa;
}
}
.illness-form {
padding: 0 15px 15px 15px;
.adm-list-header {
padding-bottom: 5px;
font-size: 16px;
font-weight: 500;
color: #121826;
border-bottom: none;
margin-top: 30px;
}
.van-field {
padding: 0;
&::after {
border-bottom: none;
}
}
.item {
> p {
color: var(--cp-text3);
padding: 15px 0;
}
}
}
}
</style>开药门诊-表单单选框处理
需求:
完成表单双向数据绑定
步骤:
- 定义枚举
- 定义选项options
- 开药门诊页面中使用
cp-radio-btn - 扩展
Consult类型添加字段,store中添加方法保存字段 - 双向数据绑定
定义枚举enums/index.ts
ts
// 肝功能
export enum LiverFunction {
/** 正常 */
Normal,
/** 异常 */
Abnormal,
/** 不清楚 */
Unclear,
}
// 肾功能
export enum RenalFunction {
/** 正常 */
Normal,
/** 异常 */
Abnormal,
/** 不清楚 */
Unclear,
}
// 过敏史
export enum AllergicHistory {
/** 正常 */
Normal,
/** 异常 */
Abnormal,
/** 不清楚 */
Unclear,
}
// 生育状态及计划
export enum FertilityStatus {
/** 无 */
No,
/** 备孕中 */
TryingToConceive,
/** 已怀孕 */
AlreadyPregnant,
/** 哺乳期 */
Breastfeeding,
}定义options选项services/constant
ts
// 肝功能
export const liverFunctionOptions = [
{ label: '正常', value: LiverFunction.Normal },
{ label: '异常', value: LiverFunction.Abnormal },
{ label: '不清楚', value: LiverFunction.Unclear },
]
// 肾功能
export const renalFunctionOptions = [
{ label: '正常', value: RenalFunction.Normal },
{ label: '异常', value: RenalFunction.Abnormal },
{ label: '不清楚', value: RenalFunction.Unclear },
]
// 过敏史
export const allergicHistoryOptions = [
{ label: '正常', value: AllergicHistory.Normal },
{ label: '异常', value: AllergicHistory.Abnormal },
{ label: '不清楚', value: AllergicHistory.Unclear },
]
// 生育状态及计划
export const fertilityStatusOptions = [
{ label: '无', value: FertilityStatus.No },
{ label: '备孕中', value: FertilityStatus.TryingToConceive },
{ label: '已怀孕', value: FertilityStatus.AlreadyPregnant },
{ label: '哺乳期中', value: FertilityStatus.Breastfeeding },
]使用cp-radio-btn在开药门诊页面中ConsultMedicine.vue
ts
import {
liverFunctionOptions,
allergicHistoryOptions,
fertilityStatusOptions,
renalFunctionOptions,
} from '@/services/constants'vue
<div class="item">
<p>肝功能</p>
<cp-radio-btn :options="liverFunctionOptions"></cp-radio-btn>
</div>
<div class="item">
<p>肾功能</p>
<cp-radio-btn :options="renalFunctionOptions"></cp-radio-btn>
</div>
<div class="item">
<p>过敏史</p>
<cp-radio-btn :options="allergicHistoryOptions"></cp-radio-btn>
</div>
<div class="item">
<p>生育状态及计划</p>
<cp-radio-btn :options="fertilityStatusOptions"></cp-radio-btn>
</div>扩展Consult类型添加字段,store中添加方法保存字段
consult.d.ts
ts
import type {
ConsultType,
IllnessTime,
OrderType,
LiverFunction,
RenalFunction,
AllergicHistory,
FertilityStatus
} from '@/enums'
...
export type Consult = {
...
/** 肝功能 */
liverFunction: LiverFunction
/** 肾功能 */
renalFunction: RenalFunction
/** 过敏史 */
allergicHistory: AllergicHistory
/** 生育状态及计划 */
fertilityStatus: FertilityStatus
}
export type MedicineIllness = Pick<
PartialConsult,
| 'illnessDesc'
| 'liverFunction'
| 'renalFunction'
| 'allergicHistory'
| 'fertilityStatus'
| 'pictures'
>src/stores/modules/consult.ts
ts
import type { ConsultIllness, PartialConsult, MedicineIllness } from '@/types/consult'
...
// 记录问药门诊病情
const setMedicineIlness = (illness: MedicineIllness) => {
consult.value.illnessDesc = illness.illnessDesc
consult.value.liverFunction = illness.liverFunction
consult.value.renalFunction = illness.renalFunction
consult.value.allergicHistory = illness.allergicHistory
consult.value.fertilityStatus = illness.fertilityStatus
consult.value.pictures = illness.pictures
}
return {
...,
setMedicineIlness
}双向数据绑定ConsultMedicine.vue
ts
import type { MedicineIllness } from '@/types/consult'
import { ref } from 'vue'
const form = ref<MedicineIllness>({
illnessDesc: '',
liverFunction: undefined,
renalFunction: undefined,
allergicHistory: undefined,
fertilityStatus: undefined,
pictures: [],
})vue
<div class="illness-form">
<div class="adm-list-header">症状描述</div>
<van-field
type="textarea"
rows="3"
placeholder="请输入所患疾病名称"
v-model="form.illnessDesc"
></van-field>
<div class="adm-list-header">用药人身体情况</div>
<div class="item">
<p>肝功能</p>
<cp-radio-btn
:options="liverFunctionOptions"
v-model="form.liverFunction"
></cp-radio-btn>
</div>
<div class="item">
<p>肾功能</p>
<cp-radio-btn
:options="renalFunctionOptions"
v-model="form.renalFunction"
></cp-radio-btn>
</div>
<div class="item">
<p>过敏史</p>
<cp-radio-btn
:options="allergicHistoryOptions"
v-model="form.allergicHistory"
></cp-radio-btn>
</div>
<div class="item">
<p>生育状态及计划</p>
<cp-radio-btn
:options="fertilityStatusOptions"
v-model="form.fertilityStatus"
></cp-radio-btn>
</div>
<div class="adm-list-header">补充病例信息</div>
<!-- 上传组件 -->
<div class="illness-img"></div>
<!-- 下一步 -->
<!-- <van-button type="primary" block round> 下一步 </van-button> -->
</div>开药门诊-封装上传组件
极速问诊中图文问诊用到了上传图片功能,在开药门诊中我们也有上传图片功能,所以需要封装上传组件
步骤:
- 封装组件
CpUpload - 为
CpUpload组件提供类型 ConsultIllness使用上传组件ConsultMedicine使用上传组件
封装组件components/CpUpload.vue
vue
<script setup lang="ts">
import { uploadImage } from '@/services/consult'
import type { UploaderFileListItem } from 'vant'
import type { UploaderAfterRead } from 'vant/lib/uploader/types'
import { ref } from 'vue'
import type { Image } from '@/types/consult'
const emit = defineEmits<{
(e: 'uploadSuccess', img: Image): void
(e: 'deleteSuccess', item: UploaderFileListItem): void
}>()
// 上传图片
const fileList = ref<Image[]>([])
// 图片上传
const onAfterRead: UploaderAfterRead = item => {
if (Array.isArray(item)) return
if (!item.file) return
item.status = 'uploading'
item.message = '上传中...'
uploadImage(item.file)
.then(res => {
item.status = 'done'
item.message = undefined
item.url = res.data.url
emit('uploadSuccess', res.data)
})
.catch(() => {
item.status = 'failed'
item.message = '上传失败'
})
}
const onDeleteImg = (item: UploaderFileListItem) => {
emit('deleteSuccess', item)
}
const setFileList = (val: Image[]) => {
fileList.value = val
}
defineExpose({
setFileList,
})
</script>
<template>
<div class="illness-img">
<van-uploader
upload-icon="photo-o"
upload-text="上传图片"
max-count="9"
:max-size="5 * 1024 * 1024"
v-model="fileList"
:after-read="onAfterRead"
@delete="onDeleteImg"></van-uploader>
<p class="tip" v-if="!fileList.length">上传内容仅医生可见,最多9张图,最大5MB</p>
</div>
</template>
<style scoped lang="scss">
.illness-img {
padding-top: 16px;
margin-bottom: 40px;
display: flex;
align-items: center;
.tip {
font-size: 12px;
color: var(--cp-tip);
}
::v-deep() {
.van-uploader {
&__preview {
&-delete {
left: -6px;
top: -6px;
border-radius: 50%;
background-color: var(--cp-primary);
width: 20px;
height: 20px;
&-icon {
transform: scale(0.9) translate(-22%, 22%);
}
}
&-image {
border-radius: 8px;
overflow: hidden;
}
}
&__upload {
border-radius: 8px;
}
&__upload-icon {
color: var(--cp-text3);
}
}
}
}
</style>为CpUpload组件提供类型src/types/components.d.ts
ts
...
import CpUpload from '@/components/CpUpload.vue'
declare module 'vue' {
interface GlobalComponents {
// 添加组件类型
...
CpUpload: typeof CpUpload
}
}ConsultIllness.vue使用上传组件
ts
// 数据的回显
onMounted(() => {
if (store.consult.illnessDesc) {
showConfirmDialog({
title: '温馨提示',
message: '是否恢复之前填写的病情信息?',
closeOnPopstate: false,
}).then(() => {
// 回显数据
const { illnessDesc, illnessTime, consultFlag, pictures } = store.consult
form.value = { illnessDesc, illnessTime, consultFlag, pictures }
cpUploadRef.value.setFileList(pictures || [])
})
}
})
const cpUploadRef = ref()
const onUploadSuccess = (image: Image) => {
form.value.pictures?.push(image)
}
const onDeleteSuccess = (item: UploaderFileListItem) => {
form.value.pictures = form.value.pictures?.filter(pic => pic.url !== item.url)
}vue
<cp-upload ref="cpUploadRef" @upload-success="onUploadSuccess" @delete-success="onDeleteSuccess"></cp-upload>ConsultMedicine.vue使用上传组件
ts
const cpUploadRef = ref()
const onUploadSuccess = (image: Image) => {
form.value.pictures?.push(image)
}
const onDeleteSuccess = (item: UploaderFileListItem) => {
form.value.pictures = form.value.pictures?.filter(pic => pic.url !== item.url)
}vue
<cp-upload ref="cpUploadRef" @upload-success="onUploadSuccess" @delete-success="onDeleteSuccess"></cp-upload>开药门诊-表单页面下一步功能
ConsultMedicine
步骤:
- 控制按钮样式
- 校验逻辑
- 下一步跳转逻辑
控制按钮样式
ts
const disabled = computed(
() =>
!form.value.illnessDesc ||
form.value.liverFunction === undefined ||
form.value.renalFunction === undefined ||
form.value.allergicHistory === undefined ||
form.value.fertilityStatus === undefined,
)vue
<van-button :class="{ disabled }" type="primary" block round @click="next">
下一步
</van-button>校验逻辑
ts
const next = () => {
if (!form.value.illnessDesc) return showToast('请输入病情描述')
if (form.value.liverFunction === undefined) return showToast('请选择肝功能情况')
if (form.value.renalFunction === undefined) return showToast('请选择肾功能情况')
if (form.value.allergicHistory === undefined) return showToast('请选择过敏史情况')
if (form.value.fertilityStatus === undefined) return showToast('请选择生育状态及计划')
}下一步跳转逻辑
ts
const store = useConsultStore()
const router = useRouter()
const next = () => {
....
// 记录病情
store.setMedicineIlness(form.value)
// 跳转,携带标识
router.push('/user/patient?isChange=1&from=medicineConsult')
}开药门诊-选择患者页面下一步功能
步骤:
- 开药门诊选择患者下一步跳转选择药品,根据标识跳转
- 新建页面结构
- 路由规则配置
开药门诊选择患者下一步跳转选择药品,根据标识跳转views/User/PatientPage.vue
ts
const fromMedicineConsultFlag = computed(() => route.query.from === 'medicineConsult')
const next = () => {
if (!patientId.value) return showToast('请选择患者')
store.setPatient(patientId.value)
if (fromMedicineConsultFlag.value) {
router.push('/consult/choose')
} else {
router.push('/consult/pay')
}
}新建页面结构ConsultChoose.vue
vue
<script setup lang="ts"></script>
<template>
<div class="consult-choose-page">
<h1>consult-choose</h1>
</div>
</template>
<style scoped lang="scss"></style>路由规则配置router/index.ts
ts
{
path: '/consult/choose',
component: () => import('@/views/Consult/ConsultChoose.vue'),
meta: { title: '选择药品' }
}选择药品-页面结构
步骤:
- 静态结构
- 步进器组件学习及样式处理
静态结构ConsultChoose.vue
vue
<script setup lang="ts">
import { showToast } from 'vant'
import { ref } from 'vue'
const searchValue = ref('')
const onSearch = (val: string) => showToast(val)
const onCancel = () => showToast('取消')
const step = ref(1)
</script>
<template>
<div class="consult-choose-page">
<cp-nav-bar title="选择药品"></cp-nav-bar>
<van-search
v-model="searchValue"
show-action
placeholder="搜一搜: 药品名称"
@search="onSearch"
@cancel="onCancel" />
<!-- 药品列表 -->
<div class="medicine-list">
<div class="item van-hairline--top" v-for="i in 20" :key="i">
<img class="img" src="@/assets/ad.png" alt="" />
<div class="info">
<p class="name">
<span>优赛明 维生素E乳 {{ i }}</span>
<span>
<van-stepper v-model="step" />
</span>
</p>
<p class="size">
<van-tag>处方药</van-tag>
<span>80ml</span>
</p>
<p class="price">¥25.00</p>
</div>
</div>
</div>
<van-action-bar>
<van-action-bar-icon icon="cart-o" badge="0" />
<div class="total-price">¥ 1000</div>
<van-action-bar-button type="primary" text="申请开方" />
</van-action-bar>
</div>
</template>
<style scoped lang="scss">
.consult-choose-page {
padding-top: 46px;
.van-search {
position: sticky;
top: 46px;
z-index: 10;
background-color: #fff;
}
.van-action-bar {
border-top: 1px solid rgba(237, 237, 237, 0.9);
.total-price {
width: 200px;
font-size: 24px;
line-height: 18px;
font-weight: 700;
color: #121826;
}
}
.medicine-list {
background-color: #fff;
padding: 0 15px 45px;
.item {
display: flex;
flex-wrap: wrap;
padding: 15px 0;
.img {
width: 80px;
height: 70px;
border-radius: 2px;
overflow: hidden;
}
.info {
padding-left: 15px;
width: 250px;
.name {
display: flex;
font-size: 15px;
margin-bottom: 5px;
> span:first-child {
// width: 200px;
// width: 300px;
width: 40vw;
}
> span:last-child {
// width: 50px;
text-align: right;
}
}
.size {
margin-bottom: 5px;
.van-tag {
background-color: var(--cp-primary);
vertical-align: middle;
}
span:not(.van-tag) {
margin-left: 10px;
color: var(--cp-tag);
vertical-align: middle;
}
}
.price {
font-size: 16px;
color: #eb5757;
}
}
.desc {
width: 100%;
background-color: var(--cp-bg);
border-radius: 4px;
margin-top: 10px;
padding: 4px 10px;
color: var(--cp-tip);
}
}
}
}
</style>步进器组件学习及样式处理
ts
const step = ref(0)vue
<van-stepper v-model="step" min="0" :class="{ hide: step === 0 }" />scss
.van-stepper {
position: absolute;
right: 0;
bottom: 15px;
:deep() {
.van-stepper__input {
background: none;
}
.van-stepper__minus {
background-color: #fff;
border: 0.5px solid #16c2a3;
}
.van-stepper__plus {
background-color: #eaf8f6;
}
.van-stepper__minus,
.van-stepper__plus {
width: 20px;
height: 20px;
}
}
&.hide {
:deep() {
.van-stepper__minus,
.van-stepper__input {
visibility: hidden;
}
}
}
}选择药品-抽离列表卡片组件
步骤:
- 抽离卡片组件
- 抽离列表组件
- 使用列表组件
抽离卡片组件views/Consult/components/MedicineCard.vue
vue
<script setup lang="ts">
import { ref } from 'vue'
const step = ref(0)
</script>
<template>
<div class="item van-hairline--top">
<img class="img" src="@/assets/ad.png" alt="" />
<div class="info">
<p class="name">
<span>优赛明 维生素E乳</span>
<span>
<van-stepper v-model="step" min="0" :class="{ hide: step === 0 }" />
</span>
</p>
<p class="size">
<van-tag>处方药</van-tag>
<span>80ml</span>
</p>
<p class="price">¥25.00</p>
</div>
</div>
</template>
<style scoped lang="scss">
.item {
display: flex;
padding: 15px 0;
.img {
width: 80px;
height: 70px;
border-radius: 2px;
overflow: hidden;
}
.info {
padding-left: 15px;
width: 250px;
.name {
display: flex;
font-size: 15px;
margin-bottom: 5px;
> span:first-child {
// width: 200px;
// width: 300px;
width: 40vw;
}
> span:last-child {
// width: 50px;
text-align: right;
}
}
.size {
margin-bottom: 5px;
.van-tag {
background-color: var(--cp-primary);
vertical-align: middle;
}
span:not(.van-tag) {
margin-left: 10px;
color: var(--cp-tag);
vertical-align: middle;
}
}
.price {
font-size: 16px;
color: #eb5757;
}
}
.desc {
width: 100%;
background-color: var(--cp-bg);
border-radius: 4px;
margin-top: 10px;
padding: 4px 10px;
color: var(--cp-tip);
}
}
.van-stepper {
position: absolute;
right: 0;
bottom: 15px;
:deep() {
.van-stepper__input {
background: none;
}
.van-stepper__minus {
background-color: #fff;
border: 0.5px solid #16c2a3;
}
.van-stepper__plus {
background-color: #eaf8f6;
}
.van-stepper__minus,
.van-stepper__plus {
width: 20px;
height: 20px;
}
}
&.hide {
:deep() {
.van-stepper__minus,
.van-stepper__input {
visibility: hidden;
}
}
}
}
</style>抽离列表组件src/views/Consult/components/MedicineList.vue
vue
<script setup lang="ts">
import MedicineCard from './MedicineCard.vue'
</script>
<template>
<div class="medicine-list">
<medicine-card v-for="i in 100" :key="i"></medicine-card>
</div>
</template>
<style scoped lang="scss">
.medicine-list {
background-color: #fff;
padding: 0 15px 45px;
}
</style>使用列表组件ConsultChoose.vue
vue
import MedicineList from './components/MedicineList.vue' ...
<!-- 药品列表 -->
<medicine-list></medicine-list>
...选择药品-list组件渲染药品列表
步骤:
- van-list初步使用
- 封装并调用请求
- 渲染药品卡片
van-list初步使用MedicineList.vue
vue
<script setup lang="ts">
import { ref } from 'vue'
import MedicineCard from './MedicineCard.vue'
const list = ref<number[]>([])
const loading = ref(false)
const finished = ref(false)
const onLoad = () => {
// 异步更新数据
// setTimeout 仅做示例,真实场景中一般为 ajax 请求
setTimeout(() => {
for (let i = 0; i < 10; i++) {
list.value.push(list.value.length + 1)
}
// 加载状态结束
loading.value = false
// 数据全部加载完成
if (list.value.length >= 40) {
finished.value = true
}
}, 1000)
}
</script>
<template>
<div class="medicine-list">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<medicine-card v-for="i in list" :key="i"></medicine-card>
</van-list>
</div>
</template>
<style scoped lang="scss">
.medicine-list {
background-color: #fff;
padding: 0 15px 45px;
}
</style>封装并调用请求
consult.d.ts编写类型,提取BasePage泛型别名
ts
import type { Medical } from './room'
// 药品列表查询参数
export type MedicineParams = PageParams & {
keyword: string
}
export type BasePage<T = any> = {
pageTotal: number
total: number
rows: T
}
// 医生列表
export type DoctorList = Doctor[]
// 医生分页
export type DoctorPage = BasePage<DoctorList>
// 文章列表
export type KnowledgeList = Knowledge[]
// 文章列表带分页
export type KnowledgePage = BasePage<KnowledgeList>
// 药品列表
export type MedicineList = Medical[]
// 药品列表带分页
export type MedicinePage = BasePage<MedicineList>封装接口service/consult.ts
ts
export const getMedicinePage = (params: MedicineParams) => {
return request<MedicinePage>('patient/medicine', 'GET', params)
}调用接口MedicineList.vue
vue
<script setup lang="ts">
import { ref } from 'vue'
import MedicineCard from './MedicineCard.vue'
import type { MedicineList, MedicineParams } from '@/types/consult'
import { getMedicinePage } from '@/services/consult'
const list = ref<MedicineList>([])
const loading = ref(false)
const finished = ref(false)
const params = ref<MedicineParams>({
keyword: '',
pageSize: 10,
current: 1,
})
const onLoad = async () => {
const { data } = await getMedicinePage(params.value)
list.value.push(...data.rows)
loading.value = false
if (params.value.current >= data.pageTotal) {
finished.value = true
} else {
finished.value = false
params.value.current++
}
}
</script>
<template>
<div class="medicine-list">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<medicine-card v-for="item in list" :key="item.id"></medicine-card>
</van-list>
</div>
</template>
<style scoped lang="scss">
.medicine-list {
background-color: #fff;
padding: 0 15px 45px;
}
</style>渲染药品卡片
父传子MedicineList.vue
vue
<medicine-card v-for="item in list" :key="item.id" :item="item"></medicine-card>子组件接收数据并渲染MedicineCard
vue
<script setup lang="ts">
import type { Medical } from '@/types/room'
import { ref } from 'vue'
defineProps<{
item: Medical
}>()
const step = ref(0)
</script>
<template>
<div class="item van-hairline--top">
<img class="img" :src="item.avatar" alt="" />
<div class="info">
<p class="name">
<span>{{ item.name }}</span>
<span>
<van-stepper v-model="step" min="0" :class="{ hide: step === 0 }" />
</span>
</p>
<p class="size">
<van-tag v-if="item.prescriptionFlag === 1">处方药</van-tag>
<span>{{ item.specs }}</span>
</p>
<p class="price">¥{{ item.amount }}</p>
</div>
</div>
</template>选择药品-搜索功能
步骤:
- 搜索框双向数据绑定字段
searchValue,keyword查询参数字段,回车后赋值该字段 - 父传子
- 监听字段,渲染页面
ConsultChoose搜索框双向数据绑定字段searchValue,keyword查询参数字段,回车后赋值该字段
ts
const searchValue = ref('')
const keyword = ref('')
const onSearch = () => {
keyword.value = searchValue.value
}
const onCancel = () => {
keyword.value = ''
}父传子ConsultChoose
vue
<!-- 药品列表 -->
<medicine-list :keyword="keyword"></medicine-list>监听字段,重新渲染页面ConsultList
vue
const props = defineProps<{
keyword: string
}>()
const params = ref<MedicineParams>({
keyword: props.keyword || '',
pageSize: 10,
current: 1
})
watch(
() => props.keyword,
(val) => {
list.value = []
params.value.keyword = val
params.value.current = 1
onLoad()
}
)选择药品-购物车底部操作栏
步骤:
Consult类型新增medicines字段store新增方法,存medicines字段- 处理药品增加数量减少数量逻辑
- 计算属性处理数量和总金额
Consult类型新增medicines字段
consult.d.ts
ts
export type Consult = {
...
/** 药品 */
medicines: Medical[]
}store新增方法,存medicines字段
ts
const setMedicines = (val: Medical[]) => (consult.value.medicines = val)
return {
...
setMedicines
...
}处理药品增加数量减少数量逻辑 MedicineCard
vue
<script setup lang="ts">
import { useConsultStore } from '@/stores'
import type { Medical } from '@/types/room'
import { ref, watch } from 'vue'
const props = defineProps<{
item: Medical
}>()
const step = ref(0)
const consultStore = useConsultStore()
const onChange = (value: string, detail: { name: string }) => {
// 不要忘记在组件上name属性绑定id值
const medicines = consultStore.consult.medicines || []
const medicine = medicines?.find(item => item.id === detail.name)
if (medicine) {
medicine.quantity = value
} else {
medicines.push({
...props.item,
quantity: value,
})
}
consultStore.setMedicines(medicines.filter(item => +item.quantity > 0))
}
watch(
() => consultStore.consult.medicines?.find(item => item.id === props.item.id),
val => {
if (val) {
step.value = +val.quantity
} else {
step.value = 0
}
},
{ deep: true, immediate: true },
)
</script>
<template>
<div class="item van-hairline--top">
<img class="img" :src="item.avatar" alt="" />
<div class="info">
<p class="name">
<span>{{ item.name }}</span>
<span>
<van-stepper :name="item.id" v-model="step" min="0" :class="{ hide: step === 0 }" @change="onChange" />
</span>
</p>
<p class="size">
<van-tag v-if="item.prescriptionFlag === 1">处方药</van-tag>
<span>{{ item.specs }}</span>
</p>
<p class="price">¥{{ item.amount }}</p>
</div>
</div>
</template>计算属性处理数量和总金额 ConsultChoose
vue
<script setup lang="ts">
....
const consultStore = useConsultStore()
const totalPrice = computed(() => {
return consultStore.consult.medicines
?.reduce((sum, item) => {
return (sum += +item.amount * +item.quantity)
}, 0)
.toFixed(2)
})
const cartLength = computed(
() =>
consultStore.consult.medicines?.length || 0
)
</script>
<template>
<div class="consult-choose-page">
<cp-nav-bar title="选择药品"></cp-nav-bar>
<van-search
v-model="searchValue"
show-action
placeholder="搜一搜: 药品名称"
@search="onSearch"
@cancel="onCancel" />
<!-- 药品列表 -->
<medicine-list :keyword="keyword"></medicine-list>
<van-action-bar>
<van-action-bar-icon
icon="cart-o"
:color="cartLength > 0 ? '#323233' : '#eee'"
:badge="cartLength === 0 ? '' : cartLength" />
<div class="total-price">¥ {{ totalPrice }}</div>
<van-action-bar-button type="primary" text="申请开方" />
</van-action-bar>
</div>
</template>选择药品-购物车抽屉
步骤:
- 抽屉结构样式
- 打开抽屉,清空药品方法定义
- 药品列表渲染
- 实现增加减少清空药品功能
抽屉结构样式ConsultChoose.vue
vue
<van-action-sheet v-model:show="show">
<div class="content">
<div class="content-header">
<div class="content-header-left">
<span>药品清单</span><span>共{{ cartLength }}件商品</span>
</div>
<div class="content-header-right" @click="clear">
<van-icon name="delete-o" />
<span>清空</span>
</div>
</div>
</div>
<van-action-bar>
<van-action-bar-icon
icon="cart-o"
:color="cartLength > 0 ? '#323233' : '#eee'"
:badge="cartLength === 0 ? '' : cartLength"
/>
<div class="total-price">¥ {{ totalPrice }}</div>
<van-action-bar-button type="primary" text="申请开方" />
</van-action-bar>
</van-action-sheet>scss
.content {
--content-height: 400px;
--content-header-height: 25px;
padding: 16px;
height: var(--content-height);
.content-header {
position: sticky;
top: 0px;
z-index: 10;
background-color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
height: var(--content-header-height);
padding-bottom: 10px;
&-left {
span {
font-size: 16px;
color: #000000;
margin-right: 10px;
}
span + span {
font-size: 13px;
color: var(--cp-primary);
}
}
&-right {
span {
margin-left: 5px;
}
}
}
.medicine-list {
padding-bottom: 45px;
}
}打开抽屉,清空药品方法定义
ts
const show = ref(false)
// 底部操作栏注册事件,注意不是抽屉内容的底部操作栏
const openCart = () => {
if (cartLength.value === 0) return showToast('请选择药品')
show.value = true
}
const clear = () => {
// console.log('clear')
show.value = false
}药品列表渲染
vue
import MedicineCard from './components/MedicineCard.vue'
<!-- 列表 -->
<div class="medicine-list">
<medicine-card
v-for="item in consultStore.consult.medicines"
:key="item.id"
:item="item"
></medicine-card>
</div>实现增加减少清空药品功能
增加减少药品功能 MedicineCard.vue
vue
watch( () => consultStore.consult.medicines?.find((item) => item.id === props.item.id), (val) => { if (val) { step.value
= +val.quantity } else { step.value = 0 } }, { deep: true, immediate: true } )清空购物车ConsultChoose
ts
const clear = () => {
// console.log('clear')
consultStore.setMedicines([])
show.value = false
}药品详情-跳转详情页
步骤:
- 基本结构,路由配置
- 跳转方法调用
- 静态结构
基本结构ConsultMedicineDetail
vue
<script setup lang="ts"></script>
<template>
<div class="medicine-detail-page">
<h1>medicine-detail</h1>
</div>
</template>
<style scoped lang="scss"></style>路由配置router/index.ts
ts
{
path: '/medicineDetail/:id',
component: () => import('@/views/Consult/ConsultMedicineDetail.vue'),
meta: { title: '药品详情' }
},跳转方法调用MedicineCard.vue
vue
<div class="item van-hairline--top" @click="$router.push(`/medicineDetail/${item.id}`)">
<img class="img" :src="item.avatar" alt="" />
<div class="info">
<p class="name">
<span>{{ item.name }}</span>
<span>
<van-stepper
:name="item.id"
v-model="step"
min="0"
:class="{ hide: step === 0 }"
@change="onChange"
@click.stop
/>
</span>
</p>
<p class="size">
<van-tag v-if="item.prescriptionFlag === 1">处方药</van-tag>
<span>{{ item.specs }}</span>
</p>
<p class="price">¥{{ item.amount }}</p>
</div>
</div>静态结构ConsultMedicineDetail.vue
vue
<script setup lang="ts"></script>
<template>
<div class="medicine-detail-page">
<cp-nav-bar title="双蚁祛湿通络胶囊(双蚁)"></cp-nav-bar>
<!-- 主图片 -->
<van-swipe indicator-color="#fff">
<van-swipe-item>
<img src="http://img.39.net/yp/360/s/863578.jpg" alt="" />
</van-swipe-item>
</van-swipe>
<div class="detail-top">
<div class="info">
<p class="name">
<span>双蚁祛湿通络胶囊(双蚁)</span>
</p>
<p class="size">
<van-tag>处方药</van-tag>
<span>每粒装0.4克</span>
</p>
<p class="price">¥186.00</p>
</div>
</div>
<div class="pay-space"></div>
<div class="detail-bottom">
<div class="info-item">
<div class="info-title">药品名称</div>
<div class="info-desc">双蚁祛湿通络胶囊(双蚁)</div>
</div>
<div class="info-item">
<div class="info-title">品牌</div>
<div class="info-desc">无</div>
</div>
<div class="info-item">
<div class="info-title">功能主治</div>
<div class="info-desc">
补肝肾、益气血、活血通络、祛风除湿的功能,临床用于改善肝肾两亏、气血不足引起的腰膝冷痛、肢气肿胀、麻木不仁、酸软乏力、屈伸不利等痹证症状。
</div>
</div>
<div class="info-item">
<div class="info-title">不良反应</div>
<div class="info-desc">尚不明确。</div>
</div>
<div class="info-item">
<div class="info-title">禁忌</div>
<div class="info-desc">尚不明确。</div>
</div>
<div class="info-item">
<div class="info-title">注意事项</div>
<div class="info-desc">1.忌与茶、牛奶同服。2.本品含有制川乌,应严格按照规定用量服用。</div>
</div>
<div class="info-item">
<div class="info-title">有效期</div>
<div class="info-desc">24 月</div>
</div>
<div class="info-item">
<div class="info-title">执行标准</div>
<div class="info-desc">国药准字J20030056</div>
</div>
<div class="info-item">
<div class="info-title">批准文号</div>
<div class="info-desc">国药准字B20021059</div>
</div>
<div class="info-item">
<div class="info-title">生产企业</div>
<div class="info-desc">企业名称:广西双蚁药业有限公司企业简称:双蚁药业</div>
</div>
</div>
<medicine-action></medicine-action>
</div>
</template>
<style scoped lang="scss">
.medicine-detail-page {
padding: 46px 0px 45px;
.van-swipe {
img {
width: 100%;
}
}
.detail-top {
padding: 15px;
.info {
padding-left: 15px;
width: 250px;
.name {
display: flex;
font-size: 15px;
margin-bottom: 5px;
> span:first-child {
width: auto;
}
> span:last-child {
text-align: right;
}
}
.size {
margin-bottom: 5px;
.van-tag {
background-color: var(--cp-primary);
vertical-align: middle;
}
span:not(.van-tag) {
margin-left: 10px;
color: var(--cp-tag);
vertical-align: middle;
}
}
.price {
font-size: 16px;
color: #eb5757;
}
}
}
.detail-bottom {
padding: 15px;
.info-item {
margin-bottom: 30px;
.info-title {
font-size: 18px;
font-family:
PingFang SC,
PingFang SC-Medium;
font-weight: 500;
color: #000000;
}
.info-desc {
margin-top: 10px;
font-size: 14px;
font-family:
PingFang SC,
PingFang SC-Regular;
font-weight: 400;
color: #3c3e42;
}
}
}
.pay-space {
height: 12px;
background-color: var(--cp-bg);
}
}
</style>药品详情-渲染页面
步骤:
- 定义类型
- 封装接口
- 调用请求渲染
定义类型consult.d.ts
ts
export type MedicineDetail = Medical & {
/** 品牌 */
brand: string
brandId?: any
/** 生产企业 */
manufacturer: string
/** 批准文号 */
approvalNo: string
/** 有效期 */
expiration: string
classify: string
classifyId: string
/** 功能主治 */
indicationsFunction: string
/** 禁忌 */
contraindication: string
/** 不良反应 */
untowardReaction: string
/** 注意事项 */
preparation: string
/** 执行标准 */
standard: string
/** 药品图片 */
mainPictures: string[]
creator: string
updator: string
createTime: string
updateTime: string
deleteState: number
}封装接口services/consult.ts
ts
export const getMedicineDetail = (id: string) => {
return request<MedicineDetail>(`patient/medicine/${id}`)
}调用请求渲染ConsultMedicineDetail.vue
vue
<script setup lang="ts">
import { getMedicineDetail } from '@/services/consult'
import type { MedicineDetail } from '@/types/consult'
import { ref } from 'vue'
import { onMounted } from 'vue'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const id = computed(() => route.params.id)
const detail = ref<MedicineDetail>()
onMounted(() => {
loadDetail()
})
const loadDetail = async () => {
const { data } = await getMedicineDetail(id.value as string)
detail.value = data
}
</script>
<template>
<div class="medicine-detail-page" v-if="detail">
<cp-nav-bar :title="detail.name"></cp-nav-bar>
<!-- 主图片 -->
<van-swipe indicator-color="#fff">
<van-swipe-item v-for="(item, index) in detail.mainPictures" :key="index">
<img :src="item" alt="" />
</van-swipe-item>
</van-swipe>
<div class="detail-top">
<div class="info">
<p class="name">
<span>{{ detail.name }}</span>
</p>
<p class="size">
<van-tag v-if="detail.prescriptionFlag === 1">处方药</van-tag>
<span>{{ detail.specs }}</span>
</p>
<p class="price">¥{{ detail.amount }}</p>
</div>
</div>
<div class="pay-space"></div>
<div class="detail-bottom">
<div class="info-item">
<div class="info-title">药品名称</div>
<div class="info-desc">{{ detail.name }}</div>
</div>
<div class="info-item">
<div class="info-title">品牌</div>
<div class="info-desc">{{ detail.brand }}</div>
</div>
<div class="info-item">
<div class="info-title">功能主治</div>
<div class="info-desc">{{ detail.indicationsFunction }}</div>
</div>
<div class="info-item">
<div class="info-title">不良反应</div>
<div class="info-desc">{{ detail.untowardReaction }}</div>
</div>
<div class="info-item">
<div class="info-title">禁忌</div>
<div class="info-desc">{{ detail.contraindication }}</div>
</div>
<div class="info-item">
<div class="info-title">注意事项</div>
<div class="info-desc">{{ detail.preparation }}</div>
</div>
<div class="info-item">
<div class="info-title">有效期</div>
<div class="info-desc">{{ detail.expiration }}</div>
</div>
<div class="info-item">
<div class="info-title">执行标准</div>
<div class="info-desc">{{ detail.standard }}</div>
</div>
<div class="info-item">
<div class="info-title">批准文号</div>
<div class="info-desc">{{ detail.approvalNo }}</div>
</div>
<div class="info-item">
<div class="info-title">生产企业</div>
<div class="info-desc">{{ detail.manufacturer }}</div>
</div>
</div>
</div>
</template>药品详情-底部操作栏功能
列表页和详情页都有底部操作栏,所以我们可以抽离组件
步骤:
- 封装底部操作栏组件
- 区分加入药箱逻辑
- 加入药箱逻辑实现
- 申请开单跳转问诊室
封装底部操作栏组件MedicineAction.vue
vue
<script setup lang="ts">
import { useConsultStore } from '@/stores'
import { computed, ref } from 'vue'
import MedicineCard from './MedicineCard.vue'
const consultStore = useConsultStore()
const totalPrice = computed(() => {
return consultStore.consult.medicines
?.reduce((sum, item) => {
return (sum += +item.amount * +item.quantity)
}, 0)
.toFixed(2)
})
const cartLength = computed(() => consultStore.consult.medicines?.length || 0)
const show = ref(false)
const openCart = () => {
if (cartLength.value === 0) return
show.value = true
}
const clear = () => {
// console.log('clear')
consultStore.setMedicines([])
show.value = false
}
</script>
<template>
<van-action-bar>
<van-action-bar-icon
icon="cart-o"
:badge="cartLength === 0 ? '' : cartLength"
:color="cartLength > 0 ? '#323233' : '#eee'"
@click="openCart" />
<div class="total-price">¥ {{ totalPrice }}</div>
<van-action-bar-button type="primary" text="申请开方" />
</van-action-bar>
<van-action-sheet v-model:show="show">
<div class="content">
<div class="content-header">
<div class="content-header-left">
<span>药品清单</span>
<span>共{{ cartLength }}件商品</span>
</div>
<div class="content-header-right" @click="clear">
<van-icon name="delete-o" />
<span>清空</span>
</div>
</div>
<!-- 列表 -->
<div class="medicine-list">
<medicine-card v-for="item in consultStore.consult.medicines" :key="item.id" :item="item"></medicine-card>
</div>
</div>
<van-action-bar>
<van-action-bar-icon
icon="cart-o"
:color="cartLength > 0 ? '#323233' : '#eee'"
:badge="cartLength === 0 ? '' : cartLength" />
<div class="total-price">¥ {{ totalPrice }}</div>
<van-action-bar-button type="primary" text="申请开方" />
</van-action-bar>
</van-action-sheet>
</template>
<style scoped lang="scss">
.van-action-bar {
border-top: 1px solid rgba(237, 237, 237, 0.9);
.total-price {
width: 200px;
font-size: 24px;
line-height: 18px;
font-weight: 700;
color: #121826;
}
}
.content {
--content-height: 400px;
--content-header-height: 25px;
padding: 16px;
height: var(--content-height);
.content-header {
position: sticky;
top: 0px;
z-index: 10;
background-color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
height: var(--content-header-height);
padding-bottom: 10px;
&-left {
span {
font-size: 16px;
color: #000000;
margin-right: 10px;
}
span + span {
font-size: 13px;
color: var(--cp-primary);
}
}
&-right {
span {
margin-left: 5px;
}
}
}
.medicine-list {
padding-bottom: 45px;
}
}
</style>ConsultMedicineDetail使用底部操作栏
vue
<medicine-action></medicine-action>区分加入药箱逻辑MedicineAction
ts
<script setup lang="ts">
...
withDefaults(
defineProps<{
from?: 'list' | 'detail'
}>(),
{
from: 'list'
}
)
const onAskDocotor = () => {
console.log('申请开方')
}
const onAddToCart = () => {
console.log('加入药箱')
}
</script>底部操作栏按钮区分,注意抽屉内容都是申请开方
vue
<van-action-bar-button v-if="from === 'list'" type="primary" text="申请开方" @click="onAskDocotor" />
<van-action-bar-button v-else type="primary" text="加入药箱" @click="onAddToCart"></van-action-bar-button>加入药箱逻辑实现,子传父通知父组件处理数据
MedicineAction
ts
const emits = defineEmits<{
(e: 'addToCart'): void
}>()
const onAddToCart = () => {
emits('addToCart')
}ConsultMedicineDetail
ts
// ConsultMedicineDetail
const consultStore = useConsultStore()
const onAddToCart = () => {
const medicines = consultStore.consult.medicines || []
const medicine = medicines?.find(item => item.id === detail.value?.id)
if (medicine) {
medicine.quantity += 1
} else {
medicines.push({
amount: detail.value?.amount!,
avatar: detail.value?.avatar!,
id: detail.value?.id!,
name: detail.value?.name!,
prescriptionFlag: detail.value?.prescriptionFlag!,
specs: detail.value?.specs!,
usageDosag: detail.value?.usageDosag!,
quantity: '1',
})
}
consultStore.setMedicines(medicines)
}申请开单跳转问诊室,需要先生成订单id,订单id的参数,我们都存在pinia中,由于不同主线需要的参数不同,所以,我们要区分下
utils/createOrderParams
ts
// utils/createOrderParams
import { ConsultType } from '@/enums'
import type { PartialConsult } from '@/types/consult'
type Key = keyof PartialConsult
// 三条线都有的字段
export const commonKeys: Key[] = [
'type', // 问医生,极速问诊,开药门诊
'illnessDesc', // 病情描述
'patientId', // 患者Id
'pictures', // 图片
]
// 极速问诊字段
export const fastKeys: Key[] = [
...commonKeys,
'depId', // 科室
'illnessTime', // 患病时长
'consultFlag', // 是否就诊过
]
// 问医生字段,比极速问诊多一个docId 医生Id
// export const doctorKeys: Key[] = [...fastKeys, 'docId']
// 开药门诊字段
export const medicineKeys: Key[] = [
...commonKeys,
'allergicHistory', // 过敏史
'fertilityStatus', // 生育状态及计划
'liverFunction', // 肝功能
'renalFunction', // 肾功能
'medicines', // 药品
]
export const getCreateOrderParams = (consult: PartialConsult, type: ConsultType = ConsultType.Fast) => {
const params: Record<string, unknown> = {}
switch (type) {
case ConsultType.Doctor:
// for (const key of doctorKeys) {
// params[key] = consult[key]
// }
break
case ConsultType.Fast:
for (const key of fastKeys) {
params[key] = consult[key]
}
break
case ConsultType.Medication:
for (const key of medicineKeys) {
params[key] = consult[key]
}
break
}
return params
}此时再处理申请开单逻辑 MedicineAction
ts
// MedicineAction
const onAskDocotor = async () => {
const medicines = consultStore.consult.medicines || []
if (medicines?.length === 0) return showToast('请先选药')
const params = getCreateOrderParams(consultStore.consult, ConsultType.Medication)
// console.log(params)
try {
const { data } = await createConsultOrder(params)
router.push(`/room?orderId=${data.id}&from=medicine`)
} catch (e) {
return showDialog({
title: '温馨提示',
message: '问诊信息不完整请重新填写',
closeOnPopstate: false,
}).then(() => {
router.push('/')
})
} finally {
consultStore.clear()
}
}问诊室-开药门诊信息渲染
步骤:
- 渲染开药门诊信息
- 数据转中文
渲染开药门诊信息 RoomMessage
ts
// RoomMessage
const route = useRoute()
const fromPage = computed(() => route.query.from)vue
<span v-if="fromPage === 'medicine'">
肝功能 {{ item.msg.consultRecord.liverFunction }} | 肾功能
{{ item.msg.consultRecord.renalFunction }} | 过敏史
{{ item.msg.consultRecord.allergicHistory }} | 生育状态
{{ item.msg.consultRecord.fertilityStatus }}
</span>
<span v-else>
{{ getIllnessTimeText(item.msg.consultRecord?.illnessTime) }} |
{{ getConsultFlagText(item.msg.consultRecord?.consultFlag) }}
</span>vue
<van-col span="6" v-if="fromPage === 'medicine'">用药需求</van-col>
<van-col span="18" v-if="fromPage === 'medicine'">
{{
item.msg.consultRecord?.medicines
.map(
(item) => `${item.name}
${item.specs} X${item.quantity}`
)
.join(',')
}}
</van-col>数据转中文
filter.ts
ts
// filter.ts
...
export const getLiverFunctionText = (val: LiverFunction) => {
return liverFunctionOptions.find((item) => item.value === val)?.label
}
export const getAllergicHistoryText = (val: AllergicHistory) => {
return allergicHistoryOptions.find((item) => item.value === val)?.label
}
export const getFertilityStatusText = (val: FertilityStatus) => {
return fertilityStatusOptions.find((item) => item.value === val)?.label
}
export const getRenalFunctionText = (val: RenalFunction) => {
return renalFunctionOptions.find((item) => item.value === val)?.label
}vue
<span v-if="fromPage === 'medicine'">
肝功能
{{ getLiverFunctionText(item.msg.consultRecord.liverFunction) }} |
肾功能
{{ getRenalFunctionText(item.msg.consultRecord.renalFunction) }} |
过敏史
{{ getAllergicHistoryText(item.msg.consultRecord.allergicHistory) }} |
生育状态
{{ getFertilityStatusText(item.msg.consultRecord.fertilityStatus) }}
</span>
📌 评论规则
需要 GitHub 账号登录 禁止发布广告、无关内容 请保持友善讨论