Skip to content
On this page

问诊室模块

image-20220824154943859

image-20220824155012193

image-20220824155114135

问诊室-路由与组件

1)路由与组件

  • 组件: views/Room/index.vue
vue
<script setup lang="ts">
import RoomStatus from "./components/RoomStatus.vue";
import RoomAction from "./components/RoomAction.vue";
import RoomMessage from "./components/RoomMessage.vue";
</script>

<template>
  <div class="room-page">
    <cp-nav-bar title="问诊室" />
    <room-status />
    <room-message />
    <room-action />
  </div>
</template>

<style lang="scss" scoped>
.room-page {
  padding-top: 90px;
  padding-bottom: 60px;
  min-height: 100vh;
  box-sizing: border-box;
  background-color: var(--cp-bg);
  .van-pull-refresh {
    width: 100%;
    min-height: calc(100vh - 150px);
  }
}
</style>
  • 路由
ts
{
      path: '/room',
      component: () => import('@/views/Room/index.vue'),
      meta: { title: '问诊室' }
    },

2)准备组件:素材文件夹中复制

  • src\views\Room\components\RoomAction.vue
  • src\views\Room\components\RoomMessage.vue
  • src\views\Room\components\RoomStatus.vue

3)准备样式文件:素材文件夹中复制

  • src\styles\room.scss

4)解读各小组件功能

问诊室-websocket 介绍

目的:认识 websocket

什么是 websocket ? https://websocket.org/

  • 是一种网络通信协议,和 HTTP 协议 一样。

为什么需要 websocket ?

  • 因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

理解 websokect 通讯过程

了解 websocket api 含义

js
// 创建ws实例,建立连接  (ws://121.40.165.18:8800  有广告)
var ws = new WebSocket("wss://javascript.info/article/websocket/demo/hello");

// 连接成功事件
ws.onopen = function (evt) {
  console.log("Connection open ...");
  // 发送消息
  ws.send("Hello WebSockets!");
};
// 接受消息事件
ws.onmessage = function (evt) {
  console.log("Received Message: " + evt.data);
  // 关闭连接
  ws.close();
};
// 关闭连接事件
ws.onclose = function (evt) {
  console.log("Connection closed.");
};

我们项目中使用 socket.io-client 来实现客户端代码,它是基于 websocket 的库。

问诊室-socket.io 使用

目的:掌握 socket.io 的基本使用

  1. socket.io 什么?

    • socket.io 是一个基于 WebSocket 的 CS(客户端-服务端)的实时通信库
    • 使用它可以在后端提供一个即时通讯服务
    • 它也提供一个 js 库,在前端可以去链接后端的 socket.io 创建的服务
    • 总结:它是一套基于 websocket 前后端即时通讯解决方案
  2. socket.io 如何使用?

  3. 我们需要掌握的客户端几个 api 的基本使用

如何使用客户端 js 库?

bash
pnpm i socket.io-client

如何建立连接?

ts
import io from "socket.io-client";
// 参数1:不传默认是当前服务域名,开发中传入服务器地址
// 参数2:配置参数,根据需要再来介绍
const socket = io();

如何确定连接成功?

ts
socket.on("connect", () => {
  // 建立连接成功
});

如何发送消息?

ts
// chat message 发送消息事件,由后台约定,可变
socket.emit("chat message", "消息内容");

如何接收消息?

ts
// chat message 接收消息事件,由后台约定,可变
socket.on("chat message", (ev) => {
  // ev 是服务器发送的消息
});

如何关闭连接?

ts
// 离开组件需要使用
socket.close();

小结:

  • sockt.io 在前端使用的 js 库需要知道哪些内容?
    • 如何建立链接 io('地址')
    • 连接成功的事件 connect
    • 如何发消息 emit + 事件
    • 如何收消息 on + 事件
    • 如果关闭连接 close()

问诊室-建立连接

步骤:

  • 安装 sokect.io-client 包
  • 在组件挂载完毕,进行 socket 连接
  • 监听连接成功
  • 组件卸载关闭连接

代码:

  • 安装 sokect.io-client 包
bash
pnpm i socket.io-client
ts
import type { Socket } from "socket.io-client";
import { io } from "socket.io-client";
import { onMounted, onUnmounted } from "vue";
import { baseURL } from "@/utils/request";
import { useUserStore } from "@/stores";
import { useRoute } from "vue-router";

const store = useUserStore();
const route = useRoute();

let socket: Socket;
onUnmounted(() => {
  socket.close();
});
onMounted( () => {
  // 建立链接,创建 socket.io 实例
  socket = io(baseURL, {
    auth: {
      token: `Bearer ${store.userInfo?.token}`,
    },
    query: {
      orderId: route.query.orderId,
    },
  });

  socket.on("connect", () => {
    // 建立连接成功
    console.log("connect");
  });

});

问诊室-通讯规则

知道前后以及定义数据的类型

  • 通讯的一些事件

image-20220829154759024

chatMsgList 接收聊天记录

sendChatMsg 发送消息

receiveChatMsg 接收消息

updateMsgStatus 消息已读

getChatMsgList 获取聊天记录

statusChange 接收订单状态改变

  • 消息数据的类型

src/enums/index.ts

ts
/** 消息类型 */
export enum MsgType {
  /** 文字聊天 */
  MsgText = 1,
  /** 图片消息 */
  MsgImage = 4,
  /** 患者信息 */
  CardPat = 21,
  /** 处方信息 */
  CardPre = 22,
  /** 未评价信息 */
  CardEvaForm = 23,
  /** 已评价信息 */
  CardEva = 24,
  /** 通用通知 */
  Notify = 31,
  /** 温馨提示 */
  NotifyTip = 32,
  /** 取消提示 */
  NotifyCancel = 33,
}

/** 处方状态 */
export enum PrescriptionStatus {
  /** 未付款 */
  NotPayment = 1,
  /** 已付款 */
  Payment = 2,
  /** 已失效 */
  Invalid = 3,
}

src/types/room.d.ts

ts
import { MsgType, PrescriptionStatus } from "@/enums";
import type { Consult, Image } from "./consult";
import type { Patient } from "./user";

export type Medical = {
  /** 药品ID */
  id: string;
  /** 药品名称 */
  name: string;
  /** 金额 */
  amount: string;
  /** 药品图片 */
  avatar: string;
  /** 规格信息 */
  specs: string;
  /** 用法用量 */
  usageDosag: string;
  /** 数量 */
  quantity: string;
  /** 是否处方,0 不是 1 是 */
  prescriptionFlag: 0 | 1;
};

export type Prescription = {
  /** 处方ID */
  id: string;
  /** 药品订单ID */
  orderId: string;
  /** 创建时间 */
  createTime: string;
  /** 患者名称 */
  name: string;
  /** 问诊记录ID */
  recordId: string;
  /** 性别 0 女 1 男 */
  gender: 0 | 1;
  /** 性别文字 */
  genderValue: "";
  /** 年龄 */
  age: number;
  /** 诊断信息 */
  diagnosis: string;
  /** 处方状态 */
  status: PrescriptionStatus;
  /** 药品清单 */
  medicines: Medical[];
};

export type EvaluateDoc = {
  /** 评价ID */
  id?: string;
  /** 评分 */
  score?: number;
  /** 内容 */
  content?: string;
  /** 创建时间 */
  createTime?: string;
  /** 创建人 */
  creator?: string;
};


// 消息类型
export type Message = {
  /** 消息ID */
  id: string;
  /** 消息类型 */
  msgType: MsgType;
  /** 发信人 */
  from?: string;
  /** 发信人ID */
  fromAvatar?: string;
  /** 收信人 */
  to?: string;
  /** 收信人头像 */
  toAvatar?: string;
  /** 创建时间 */
  createTime: string;
  /** 消息主体 */
  msg: {
    /** 文本内容 */
    content?: string;
    /** 图片对象 */
    picture?: Image;
    /** 问诊记录,患者信息 */
    consultRecord?: Consult & {
      patientInfo: Patient;
    };
    /** 处方信息 */
    prescription?: Prescription;
    /** 评价信息 */
    evaluateDoc?: EvaluateDoc;
  };
};

/** 消息分组列表 */
export type TimeMessages = {
  /** 分组消息最早时间 */
  createTime: string;
  /** 消息数组 */
  items: Message[];
  /** 订单ID */
  orderId: string;
  /** 会话ID */
  sid: string;
};

以上是类型

问诊室-默认消息

步骤:

  • 监听默认聊天记录,并且处理处理成消息列表
  • 提取常量数据
  • 进行渲染
  • 预览病情图片

代码:

1)监听默认聊天记录,并且处理处理成消息列表 Room/index.vue

ts
import { MsgType } from "@/enums";
import type { Message, TimeMessages } from "@/types/room";

const list = ref<Message[]>([]);
ts
// 聊天记录
socket.on("chatMsgList", ({ data }: { data: TimeMessages[] }) => {  
    list.value.push(...data[0].items)
});
vue
<room-message :list="list" />

3)进行渲染 Room/components/RoomMessage.vue

ts
defineProps<{ list: Message[] }>();

const timeOptions = [
  { label: '一周内', value: IllnessTime.Week },
  { label: '一月内', value: IllnessTime.Month },
  { label: '半年内', value: IllnessTime.HalfYear },
  { label: '大于半年', value: IllnessTime.More }
]
const flagOptions = [
  { label: '就诊过', value: 0 },
  { label: '没就诊过', value: 1 }
]

const getIllnessTimeText = (time: IllnessTime) => {
  return timeOptions.find((item) => item.value === time)?.label
}

const getConsultFlagText = (flag: 0 | 1) => {
  return flagOptions.find((item) => item.value === flag)?.label
}
html
<template>
  <div v-for="{ msgType, msg, createTime, from } in list" :key="id">
    <!-- 病情描述 - 💥💥注意不能用v-show -->
    <div class="msg msg-illness" v-if="msgType === MsgType.CardPat">
      <div class="patient van-hairline--bottom" v-if="msg.consultRecord">
        <p>
          {{ msg.consultRecord?.patientInfo.name }} {{
          msg.consultRecord?.patientInfo.genderValue }} {{
          msg.consultRecord?.patientInfo.age }}岁
        </p>
        <p>
          {{ getIllnessTimeText(msg.consultRecord!.illnessTime) }} | {{
          getConsultFlagText(msg.consultRecord!.consultFlag) }}
        </p>
      </div>
      <van-row>
        <van-col span="6">病情描述</van-col>
        <van-col span="18">{{ msg.consultRecord?.illnessDesc }}</van-col>
        <van-col span="6">图片</van-col>
        <van-col span="18" @click="onPreviewImg(msg.consultRecord?.pictures)">
          点击查看
        </van-col>
      </van-row>
    </div>
    <!-- 温馨提示 -->
    <div class="msg msg-tip" v-if="msgType === MsgType.NotifyTip">
      <div class="content">
        <span class="green">温馨提示:</span>
        <span>{{ msg.content }}</span>
      </div>
    </div>
    <!-- 通用通知 -->
    <div class="msg msg-tip" v-if="msgType === 31">
      <div class="content">
        <span>{{ msg.content }}</span>
      </div>
    </div>
  </div>
</template>

4)预览病情图片 Room/components/RoomMessage.vue

ts
import { showImagePreview } from "vant";

const onPreview = (pics?: Image[]) => {
  if (!pics || !pics.length) return
  const urls = pics.map((item) => item.url)
  showImagePreview(urls)
}

问诊室-接诊状态

步骤:

  • 初始化需要订单信息,订单状态发送变化,需要更新订单信息
  • 状态组件,根据状态展示对应信息
    • 待接诊,绿色文字提示
    • 问诊中,倒计时显示
    • 已结束 or 已取消,显示问诊结束
  • 底部操作组件,禁用和启用

代码:

  1. 订单状态发送变化,需要更新组件 Room/index.vue

enums/index.ts

ts
/** 问诊订单的状态 */
export enum OrderType {
  /** 待支付 */
  ConsultPay = 1,
  /** 待接诊 */
  ConsultWait = 2,
  /** 问诊中 */
  ConsultChat = 3,
  /** 问诊完成 */
  ConsultComplete = 4,
  /** 取消问诊 */
  ConsultCancel = 5,
  /** 药品订单 */
  /** 待支付 */
  MedicinePay = 10,
  /** 待发货 */
  MedicineSend = 11,
  /** 待收货 */
  MedicineTake = 12,
  /** 已完成 */
  MedicineComplete = 13,
  /** 取消订单 */
  MedicineCancel = 14,
}

types/consult.d.ts

ts
/** 问诊订单单项信息 */
export type ConsultOrderItem = Consult & {
  /** 创建时间 */
  createTime: string;
  /** 医生信息 */
  docInfo?: Doctor;
  /** 患者信息 */
  patientInfo: Patient;
  /** 订单编号 */
  orderNo: string;
  /** 订单状态 */
  status: OrderType;
  /** 状态文字 */
  statusValue: string;
  /** 类型问诊文字 */
  typeValue: string;
  /** 倒计时时间 */
  countdown: number;
  /** 处方ID */
  prescriptionId?: string;
  /** 评价ID */
  evaluateId: number;
  /** 应付款 */
  payment: number;
  /** 优惠券抵扣 */
  couponDeduction: number;
  /** 积分抵扣 */
  pointDeduction: number;
  /** 实付款 */
  actualPayment: number;
};

services/consult.ts

ts
/** 获取订单详情API */
export const getOrderDetailAPI = (orderId: string) => {
  return request({ url: '/patient/consult/order/detail', params: { orderId } })
}
ts
import type { ConsultOrderItem } from "@/types/consult";
import { getOrderDetailAPI } from "@/services/consult";

const consult = ref<ConsultOrderItem>();
const loadDetailData = async () => {
  const res = await getOrderDetailAPI(route.query.orderId as string);
  consult.value = res.data;
};

onMounted(loadDetailData);
ts
// 订单状态 在onMounted注册
socket.on("statusChange", loadDetailData);

2)传入 订单状态 倒计时时间 给状态组件 Room/index.vue

html
<room-status :status="consult?.status" :countdown="consult?.countdown" />

Room/components/RoomStatus.vue

ts
import { OrderType } from "@/enums";

interface Props {
  status?: OrderType;
  countdown?: number;
}

const { status, countdown = 0 } = defineProps<Props>();

开启解构 Props 响应式转换功能,vite.config.ts

vue({ reactivityTransform: true }),

3)根据状态展示对应信息 Room/components/RoomStatus.vue

html
<div class="room-status">
  <div class="wait" v-if="status === OrderType.ConsultWait">
    已通知医生尽快接诊,24小时内医生未回复将自动退款
  </div>
  <div class="chat" v-if="status === OrderType.ConsultChat">
    <span>咨询中</span>
    <span class="time"
      >剩余时间:<van-count-down :time="countdown * 1000"
    /></span>
  </div>
  <div
    class="end"
    v-if="status === OrderType.ConsultComplete || status === OrderType.ConsultCancel"
  >
    <van-icon name="passed" /> 已结束
  </div>
</div>
  1. 根据状态禁用状态栏 Room/components/RoomAction.vue
html
<room-action
  :disabled="consult?.status !== OrderType.ConsultChat"
></room-action>
ts
defineProps<{ disabled: boolean }>();
diff
<van-field
+     :disabled="disabled"
      type="text"
      class="input"
      :border="false"
      placeholder="问医生"
      autocomplete="off"
    ></van-field>
    <!-- 不预览,使用小图标作为上传按钮 -->
+    <van-uploader :preview-image="false" :disabled="disabled">
      <cp-icon name="consult-img" />
    </van-uploader>

问诊室-文字聊天

可以发送文字消息,可以接收文字消息

步骤:

  • 声明数据 text,双向绑定输入框
  • 点击发送,子传父,触发 send-text 事件
  • 问诊室组件,监听 send-text 事件接收文字
  • 获取订单详情,需要使用医生 ID,作为收信息人的标识
  • 通过 socket.emitsendChatMsg 发送文字给服务器
  • 通过 socket.onreceiveChatMsg 接收发送成功或者医生发来的消息
  • 展示消息

代码:

1)声明数据 text,双向绑定输入框

2)点击发送,子传父,触发 send-text 事件

Room/components/RoomAction.vue

ts
import { ref } from "vue";
const text = ref("");

const emit = defineEmits<{
  (e: "send-text", text: string): void;
}>();

const onSendText = () => {
  emit("send-text", text.value);
  text.value = "";
};
diff
<van-field
      :disabled="disabled"
+     v-model="text"
      type="text"
      class="input"
      :border="false"
      placeholder="问医生"
      autocomplete="off"
+     @keyup.enter="onSendText"
    />

2)问诊室组件,监听 send-text 事件接收文字 Room/index.vue

html
<room-action @send-text="sendText" />
ts
const sendText = (text: string) => {
  // 发送消息
};

3)通过 socket.emitsendChatMsg 发送文字给服务器

ts
const sendText = (text: string) => {
  // 发送信息需要  发送人  收消息人  消息类型  消息内容
  socket.emit("sendChatMsg", {
    from: store.userInfo?.id,
    to: consult.value?.docInfo?.id,
    msgType: MsgType.MsgText,
    msg: { content: text },
  });
};

5)通过 socket.onreceiveChatMsg 接收发送成功或者医生发来的消息

ts
// 接收消息 在onMounted注册
socket.on("receiveChatMsg", async (event) => {
  list.value.push(event);
  nextTick(() => {
    window.scrollTo(0, document.body.scrollHeight);
  });
});
  1. 展示消息 Room/components/RoomMessage.vue
bash
pnpm i dayjs
ts
import dayjs from "dayjs";

const formatTime = (time: string) => dayjs(time).format("HH:mm");
html
<!-- 我发的消息 -->
<div
  class="msg msg-to"
  v-if="msgType === MsgType.MsgText && store.userInfo?.id === from"
>
  <div class="content">
    <div class="time">{{ formatTime(createTime) }}</div>
    <div class="pao">{{ msg.content }}</div>
  </div>
  <van-image :src="store.userInfo?.avatar" />
</div>

<!-- 医生发的消息 -->
<div
  class="msg msg-from"
  v-if="msgType === MsgType.MsgText && store.user?.id !== from"
>
  <van-image :src="fromAvatar" />
  <div class="content">
    <div class="time">{{ formatTime(createTime) }}</div>
    <div class="pao">{{ msg.content }}</div>
  </div>
</div>

问诊室-图片聊天

步骤:

  • 底部操作组件,可以上传图片,触发 send-image 事件传出图片对象
  • 问诊室组件,监听 send-image 事件接收图片对象
  • 通过 socket.emitsendChatMsg 发送图片给服务器
  • 展示消息

代码:

1)底部操作组件,可以上传图片,触发 send-image 事件传出图片对象

ts
import { uploadImageAPI } from "@/services/consult";
import type { Image } from "@/types/consult";
import type { UploaderAfterRead } from "vant/lib/uploader/types";
diff
interface Emits {
  (e: 'send-text', text: string): void
+ (e: 'send-image', img: Image): void
}
ts
const onSendImage: UploaderAfterRead = async (data) => {
  if (Array.isArray(data)) return;
  if (!data.file) return;
  const t = showLoadingToast("正在上传");
  const res = await uploadImageAPI(data.file);
  t.close();
  emit("send-image", res.data);
};
diff
<van-uploader
	:preview-image="false"
  :disabled="disabled"
+ :after-read="onSendImage"
  >
  <cp-icon name="consult-img" />
</van-uploader>

2)问诊室组件,监听 send-image 事件接收图片对象,通过 socket.emitsendChatMsg 发送图片给服务器

diff
<room-action
      :disabled="consult?.status !== OrderType.ConsultChat"
      @send-text="sendText"
+     @send-image="onSendImage"
 />
ts
import type { Image } from "@/types/consult";

const onSendImage = (img: Image) => {
  socket.emit("sendChatMsg", {
    from: store.userInfo?.id,
    to: consult.value?.docInfo?.id,
    msgType: MsgType.MsgImage,
    msg: { picture: img },
  });
};

3)展示消息

html
<!-- 发消息-图片 -->
<div
  class="msg msg-to"
  v-if="msgType === MsgType.MsgImage && store.userInfo?.id === from"
>
  <div class="content">
    <div class="time">{{ formatTime(createTime) }}</div>
    <van-image fit="contain" :src="msg.picture?.url" />
  </div>
  <van-image :src="store.userInfo?.avatar" />
</div>

<!-- 收消息-图片 -->
<div
  class="msg msg-from"
  v-if="msgType === MsgType.MsgImage && store.userInfo?.id !== from"
>
  <van-image :src="fromAvatar" />
  <div class="content">
    <div class="time">{{ formatTime(createTime) }}</div>
    <van-image fit="contain" :src="msg.picture?.url" />
  </div>
</div>

问诊室-聊天记录

流程:

image-20230216235123972

步骤:

  • 实现下拉刷新效果
  • 记录每段消息的开始时间,作为下一次请求的开始时间
  • 触发刷新,发送获取聊天记录消息
  • 在接收聊天记录事件中
    • 关闭刷新中效果
    • 判断是否有数据?没有提示 没有聊天记录了
    • 如果是初始化获取的聊天,需要滚动到最底部
    • 如果是第二,三...次获取消息,不需要滚动到底部
  • 如果断开连接后再次连接,需要清空聊天记录

代码:

  • 实现下拉刷新效果
html
<van-pull-refresh v-model="loading" @refresh="onRefresh">
  <room-message :list="list" />
</van-pull-refresh>
ts
const loading = ref(false);
const onRefresh = () => {
  // 触发下拉
};
  • 记录每段消息的开始时间,作为下一次请求的开始时间
ts
const nextTime = ref('');
diff
socket.on('chatMsgList', ({ data }: { data: TimeMessages[] }) => {
+    loading.value = false
+    nextTime.value = data[0].items[0].createTime
    list.value.push(...data[0].items)

  })
  • 触发刷新,发送获取聊天记录消息
ts
const onRefresh = () => {
  socket.emit("getChatMsgList", 20, time.value, route.query.orderId);
};
  • 在接收聊天记录事件中
    • 关闭刷新中效果
    • 判断是否有数据?没有提示 没有聊天记录了
    • 如果是初始化获取的聊天,需要滚动到最底部
ts
const finished = ref(false)
diff
socket.on('chatMsgList', ({ data }: { data: TimeMessages[] }) => {
    loading.value = false

+    if (!data.length) {
+      finished.value = true
+      return showToast('没有更多数据了')
+    }

    nextTime.value = data[0].items[0].createTime
+   if (list.value.length) {
+      list.value.unshift(...data[0].items)
+    } else {
+      list.value.push(...data[0].items)
+      nextTick(() => {
+        // 2. 第一次加载后滚动到底部
+        window.scrollTo(0, document.body.scrollHeight)
+      })
+    }
  })
  • 如果断开连接后再次连接,需要清空聊天记录
ts
// 建立连接成功
socket.on("connect", () => {
  list.value = [];
});

问诊室-查看处方

步骤:

  • 定义参考处方 API
  • 点击查看处方预览处方图片

TIP

需要去辅助-超级医生开处方

代码:

  1. 渲染处方消息
html
<!-- 处方 -->
<div class="msg msg-recipe" v-if="msgType === MsgType.CardPre">
  <div class="content" v-if="msg.prescription">
    <div class="head van-hairline--bottom">
      <div class="head-tit">
        <h3>电子处方</h3>
        <p >
          原始处方 <van-icon name="arrow"></van-icon>
        </p>
      </div>
      <p>
        {{ msg.prescription.name }} {{ msg.prescription.genderValue }} {{
        msg.prescription.age }}岁 {{ msg.prescription.diagnosis }}
      </p>
      <p>开方时间:{{ msg.prescription.createTime }}</p>
    </div>
    <div class="body">
      <div
        class="body-item"
        v-for="med in msg.prescription.medicines"
        :key="med.id"
      >
        <div class="drug">
          <p>{{ med.name }} {{ med.specs }}</p>
          <p>{{ med.usageDosag }}</p>
        </div>
        <div class="num">x{{ med.quantity }}</div>
      </div>
    </div>
    <div class="foot">
      <span>购买药品</span>
    </div>
  </div>
</div>

2)定义参考处方 API

services/consult.ts

ts
/** 查看处方API */
export const getPrescriptionPicAPI = (id: string) => {
  return request(`/patient/consult/prescription/${id}`)
}

3)点击查看处方预览处方图片

diff
<div class="head-tit">
            <h3>电子处方</h3>
+            <p @click="showPrescription(msg.prescription?.id)">
              原始处方 <van-icon name="arrow"></van-icon>
            </p>
          </div>
ts
import { getPrescriptionPicAPI } from "@/services/consult";
import { showImagePreview } from "vant";

const showPrescription = async (id?: string) => {
  if (id) {
    const res = await getPrescriptionPicAPI(id);
    showImagePreview([res.data.url]);
  }
};

问诊室-购买药品

步骤:

  • 处方状态不同此,按钮操作不同:
    • 如果处方失效:提示即可
    • 如果没付款且有订单 ID,代表已经生成订单没付款:去订单详情付款
    • 如果没付款且没订单 ID:去预支付页面

代码:

按钮事件绑定

html
<div class="foot"><span @click="onBuy(msg.prescription!)">购买药品</span></div>

跳转逻辑处理

ts
import { useRouter } from "vue-router";
import { PrescriptionStatus } from "@/enums";

// 点击处方的跳转
const router = useRouter();
const onBuy = (pre: Prescription) => {
  if (pre.status === PrescriptionStatus.Invalid) return showToast("处方已失效");
  if (pre.status === PrescriptionStatus.NotPayment && !pre.orderId)
    return router.push(`/order/pay?id=${pre.id}`);
  router.push(`/order/${pre.orderId}`);
};

问诊室-评价医生

步骤:

  • 准备评价组件:素材复制
  • 展示评价组件
    • 传入 评价信息对象 条件展示
  • 评价表单数据绑定和校验
  • 提交评价
  • 修改消息

代码:

1)准备评价组件 Room/components/EvaluateCard.vue

2)展示评价组件

Room/components/RoomMessage.vue

diff
<div
      class="msg msg-comment"
+      v-if="msgType === MsgType.CardEva || msgType === MsgType.CardEvaForm"
    >
+      <evaluate-card :evaluateDoc="msg.evaluateDoc" />
    </div>

Room/components/evaluateCard.vue

ts
import type { EvaluateDoc } from "@/types/room";

defineProps<{
  evaluateDoc?: EvaluateDoc;
}>();
diff
+  <div class="evaluate-card" v-if="evaluateDoc">
    <p class="title">医生服务评价</p>
    <p class="desc">我们会更加努力提升服务质量</p>
    <van-rate
-    	:modelValue="3"
+     :modelValue="evaluateDoc.score"
      size="7vw"
      gutter="3vw"
      color="#FADB14"
      void-icon="star"
      void-color="rgba(0,0,0,0.04)"
    />
  </div>
+  <div class="evaluate-card" v-else>

3)评价表单数据绑定和校验

ts
import { computed, inject, ref } from "vue";

const score = ref(0);
const anonymousFlag = ref(false);
const content = ref("");
const disabled = computed(() => !score.value || !content.value);

const onSubmit = async () => {
  if (!score.value) return showToast("请选择评分");
  if (!content.value) return showToast("请输入评价");
};
diff
<van-rate
+     v-model="score"
      size="7vw"
      gutter="3vw"
      color="#FADB14"
      void-icon="star"
      void-color="rgba(0,0,0,0.04)"
    />
    <van-field
+     v-model="content"
      type="textarea"
      maxlength="150"
      show-word-limit
      rows="3"
      placeholder="请描述您对医生的评价或是在医生看诊过程中遇到的问题"
    />
    <div class="footer">
      <van-checkbox
+      	v-model="anonymousFlag"
      >匿名评价</van-checkbox>
      <van-button
+      	@click="onSubmit"
      	type="primary"
        size="small"
+       :class="{ disabled }"
        round
      >
        提交评价
      </van-button>
    </div>

4)提交评价

api 函数

ts
type EvaluateOrderParams = {
  docId: string;
  orderId: string;
  score: number;
  content: string;
  anonymousFlag: 0 | 1;
};

// 评价问诊
export const evaluateOrderAPI = (data: EvaluateOrderParams) => {
  return request({ url: "/patient/order/evaluate", method: "POST", data });
};

注入订单信息:提供医生 ID 和订单 ID Room/index.vue

ts
import { provide } from "vue";
// ...
provide("consult", consult);

Room/components/EvaluateCard.vue

ts
import { inject, type Ref } from "vue";
import type { ConsultOrderItem } from "@/types/consult";

const consult = inject<Ref<ConsultOrderItem>>("consult");

提交评价

diff
const onSubmit = async () => {
  if (!score.value) return showToast('请选择评分')
  if (!content.value) return showToast('请输入评价')
+  if (!consult?.value) return showToast('未找到订单')
+  if (consult.value.docInfo?.id) {
+    await evaluateOrderAPI({
+      docId: consult.value?.docInfo?.id,
+      orderId: consult.value?.id,
+      score: score.value,
+      content: content.value,
+      anonymousFlag: anonymousFlag.value ? 1 : 0
+    })
+  }
  // 修改消息
}

5)成功后修改消息 Room/index.vue

ts
const completeEva = (score: number) => {
  const item = list.value.find((item) => item.msgType === MsgType.CardEvaForm);
  if (item) {
    item.msg.evaluateDoc = { score };
    item.msgType = MsgType.CardEva;
  }
};
provide("completeEva", completeEva);

Room/components/EvaluateCard.vue

diff
+const completeEva = inject<(score: number) => void>('completeEva')


const onSubmit = async () => {
  if (!score.value) return showToast('请选择评分')
  if (!content.value) return showToast('请输入评价')
  if (!consult?.value) return showToast('未找到订单')
  if (consult.value.docInfo?.id) {
    await evaluateOrderAPI({
      docId: consult.value.docInfo?.id,
      orderId: consult.value?.id,
      score: score.value,
      content: content.value,
      anonymousFlag: anonymousFlag.value ? 1 : 0
    })
  }
+  completeEva && completeEva(score.value)
}

结束问诊消息:

Room/components/RoomMessage.vue

html
<div class="msg msg-tip msg-tip-cancel" v-if="msgType === MsgType.NotifyCancel">
  <div class="content">
    <span>{{ msg.content }}</span>
  </div>
</div>

问诊室-支付失败

处理问诊室支付失败情况

思考:

  • 怎么处理?

    • 地址栏上的 payResult 是否是 false,是代表失败
  • 何时处理?

    • 组件挂载完毕,太晚,页面已渲染
    • 进入路由前处理即可
diff
{
      path: '/room',
      component: () => import('@/views/Room/index.vue'),
      meta: { title: '问诊室' },
+      beforeEnter(to) {
+        if (to.query.payResult === 'false') return '/user/consult'
+      }
    },

Released under the MIT License.