uni-app(优医咨询)项目实战 - 第6天

2024-04-25 14:12:15 浏览数 (2)

title: uni-app(优医咨询)项目实战 - 第6天 series: uni-app(优医咨询)项目实战 abbrlink: 33b075a1 date: 2024-04-17 11:01:42

uni-app(优医咨询)项目实战 - 第6天

学习目标:

  • 掌握第三方支付的流程
  • 能够使用支付宝完成支付
  • 了解 uni-pay 聚合支付的使用步骤
  • 知道如何通过 websocket 进行通信
  • 能够完成问诊订单的创建
一、极速问诊

继续完善极速问诊的相关功能。

1.1 选择患者

在患者列表中选择需要问诊的患者,访部分的逻辑在家庭档案管理模块已经实现过了,偷懒将之前的代码拷贝过来(当然也可进行更完善的封装)。

1.1.1 布局模板
代码语言:javascript复制
<!-- subpkg_consult/patient/index.vue -->
<script setup>
  import { ref } from 'vue'
	
  // 侧滑按钮配置
  const swipeOptions = ref([
    {
      text: '删除',
      style: {
        backgroundColor: '#dd524d',
      },
    },
  ])
</script>
<template>
  <scroll-page>
    <view class="patient-page">
      <view class="page-header">
        <view class="patient-title"> 请选择患者信息 </view>
        <view class="patient-tips">
          以便医生给出更准确的治疗,信息仅医生可见
        </view>
      </view>
      <uni-swipe-action>
        <uni-swipe-action-item :right-options="swipeOptions">
          <view class="archive-card active">
            <view class="archive-info">
              <text class="name">李富贵</text>
              <text class="id-card">321***********6164</text>
              <text class="default">默认</text>
            </view>
            <view class="archive-info">
              <text class="gender">男</text>
              <text class="age">32岁</text>
            </view>
            <navigator
              hover-class="none"
              class="edit-link"
              url="/subpkg_archive/form/index"
            >
              <uni-icons
                type="icon-edit"
                size="20"
                color="#16C2A3"
                custom-prefix="iconfont"
              />
            </navigator>
          </view>
        </uni-swipe-action-item>
      </uni-swipe-action>

      <!-- 添加按钮 -->
      <view v-if="true" class="archive-card">
        <navigator
          class="add-link"
          hover-class="none"
          url="/subpkg_archive/form/index"
        >
          <uni-icons color="#16C2A3" size="24" type="plusempty" />
          <text class="label">添加患者</text>
        </navigator>
      </view>
    </view>
    <!-- 下一步操作 -->
    <view class="next-step">
      <button class="uni-button">
        下一步
      </button>
    </view>
  </scroll-page>
</template>

<style lang="scss">
  @import './index.scss';
</style>
代码语言:javascript复制
// subpkg_consult/patient/index.scss
.patient-page {
  padding: 30rpx 30rpx calc(env(safe-area-inset-bottom)   200rpx);
}

.page-header {
  margin-top: 20rpx;
  margin-bottom: 40rpx;
}

.patient-title {
  font-size: 36rpx;
  color: #121826;
}

.patient-tips {
  margin-top: 10rpx;
  font-size: 26rpx;
  color: #6f6f6f;
}

.archive-card {
  display: flex;
  flex-direction: column;
  justify-content: center;

  position: relative;

  height: 180rpx;
  padding: 30rpx;
  margin-bottom: 30rpx;
  border-radius: 10rpx;
  box-sizing: border-box;
  border: 1rpx solid transparent;
  background-color: #f6f6f6;

  &.active {
    background-color: rgba(44, 181, 165, 0.1);
    // border: 1rpx solid #16c2a3;
  }

  .archive-info {
    display: flex;
    align-items: center;
    color: #6f6f6f;
    font-size: 28rpx;
    margin-bottom: 10rpx;
  }

  .name {
    margin-right: 30rpx;
    color: #121826;
    font-size: 32rpx;
    font-weight: 500;
  }

  .id-card {
    color: #121826;
  }

  .gender {
    margin-right: 30rpx;
  }

  .default {
    height: 36rpx;
    line-height: 36rpx;
    text-align: center;
    padding: 0 12rpx;
    margin-left: 30rpx;
    border-radius: 4rpx;
    color: #fff;
    font-size: 24rpx;
    background-color: #16c2a3;
  }
}

.edit-link {
  position: absolute;
  top: 50%;
  right: 30rpx;

  transform: translateY(-50%);
}

.add-link {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

  .label {
    margin-top: 10rpx;
    font-size: 28rpx;
    color: #16c2a3;
  }
}

.next-step {
  position: fixed;
  right: 0;
  left: 0;
  bottom: 0;
  padding: 30rpx 40rpx calc(env(safe-area-inset-bottom)   30rpx);
  background-color: #fff;
}

:deep(.uni-swipe_button-group) {
  bottom: 30rpx;
}
1.1.2 患者列表

患者列表的接口在前面家庭档案模块中已经封装过了,在此直接调用获取数据即可。

代码语言:javascript复制
<!-- subpkg_consult/patient/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { onShow } from '@dcloudio/uni-app'
  import { patientListApi } from '@/services/patient'
  // 侧滑按钮配置
  const swipeOptions = ref([
    {
      text: '删除',
      style: {
        backgroundColor: '#dd524d',
      },
    },
  ])
  // 患者列表
  const patientList = ref([])
  // 是否显示页面内容
  const pageShow = ref(false)

  // 页面加载生命周期
  onShow(() => {
    // 获取患者列表
    getPatientList()
  })

  // 家庭档案(患者)列表
  async function getPatientList() {
    // 患者列表接口
    const { code, data } = await patientListApi()
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.showToast('列表获取失败,稍后重试!')
    // 渲染接口数据
    patientList.value = data
    // 展示页面内容
    pageShow.value = true
  }
</script>

<template>
  <scroll-page>
    <view class="patient-page">
      <view class="page-header">
        <view class="patient-title"> 请选择患者信息 </view>
        <view class="patient-tips">
          以便医生给出更准确的治疗,信息仅医生可见
        </view>
      </view>
      <uni-swipe-action>
        <uni-swipe-action-item
          v-for="(patient, index) in patientList"
          :key="patient.id"
          :right-options="swipeOptions"
        >
          <view class="archive-card">
            <view class="archive-info">
              <text class="name">{{ patient.name }}</text>
              <text class="id-card">
                {{ patient.idCard.replace(/^(.{6}). (.{4})$/, '$1********$2') }}
              </text>
              <text v-if="patient.defaultFlag === 1" class="default">默认</text>
            </view>
            <view class="archive-info">
              <text class="gender">{{ patient.genderValue }}</text>
              <text class="age">{{ patient.age }}岁</text>
            </view>
            <navigator
              class="edit-link"
              :url="`/subpkg_archive/add/index?id=${patient.id}`"
            >
              <uni-icons
                type="icon-edit"
                size="20"
                color="#16C2A3"
                custom-prefix="iconfont"
              />
            </navigator>
          </view>
        </uni-swipe-action-item>
      </uni-swipe-action>
      <!-- 添加按钮 -->
      <view  v-if="patientList.length < 6" class="archive-card">
        <navigator
          class="add-link"
          hover-class="none"
          url="/subpkg_archive/form/index"
        >
          <uni-icons color="#16C2A3" size="24" type="plusempty" />
          <text class="label">添加患者</text>
        </navigator>
      </view>
    </view>
    <!-- 下一步操作 -->
    <view class="next-step">
      <navigator class="uni-button" url="/subpkg_consult/payment/index">
        下一步
      </navigator>
    </view>
  </scroll-page>
</template>
1.2.3 选择患者

用户通过点击的方式选择就诊患者,被选中的患者需要以高亮的方式显示(添加 .active 类名),实现步骤:

  1. 监听点击事件
  2. 根据索引值设置高亮样式
  3. 根据索引值获取患者ID
代码语言:javascript复制
<!-- subpkg_consult/patient/index.vue -->
<script setup>
  import { ref, computed } from 'vue'
  import { onShow } from '@dcloudio/uni-app'
  import { patientListApi } from '@/services/patient'
	
  // 省略前面小节的代码...
  
  // 患者卡片索引值
  const patientCardIndex = ref(0)
  
  // 所选患者的ID
  const patientId = computed(() => {
    return patientList.value[patientCardIndex.value].id
  })
	
  // 省略前面小节的代码

  function onPatientCardClick(index) {
    // 患者的索引值
    patientCardIndex.value = index
  }

	// 省略前面小节的代码...
</script>

<template>
  <scroll-page>
    <view class="patient-page" v-if="pageShow">
      <view class="page-header">
        <view class="patient-title"> 请选择患者信息 </view>
        <view class="patient-tips">
          以便医生给出更准确的治疗,信息仅医生可见
        </view>
      </view>

      <uni-swipe-action>
        <uni-swipe-action-item
          v-for="(patient, index) in patientList"
          :key="patient.id"
          :right-options="swipeOptions"
        >
          <view
            @click="onPatientCardClick(index)"
            :class="{ active: patientCardIndex === index }"
            class="archive-card"
          >
            <!-- 省略前面小节的代码... -->
          </view>
        </uni-swipe-action-item>
      </uni-swipe-action>

      <!-- 添加按钮 -->
      <view v-if="patientList.length < 6" class="archive-card">
        <navigator
          class="add-link"
          hover-class="none"
          url="/subpkg_archive/form/index"
        >
          <uni-icons color="#16C2A3" size="24" type="plusempty" />
          <text class="label">添加患者</text>
        </navigator>
      </view>
    </view>
    <!-- 下一步操作 -->
    <view class="next-step">
      <navigator class="uni-button" url="/subpkg_consult/payment/index">
        下一步
      </navigator>
    </view>
  </scroll-page>
</template>
1.2 预付订单

选择患者后的下一个步骤是创建问诊订单。按下面的分包配置创建分包页面,先创建好页面再来补充配置:

代码语言:javascript复制
{
  "subPackages": [
    {
      "root": "subpkg_consult",
      "pages": [
        {
          "path": "payment/index",
          "style": {
            "navigationBarTitleText": "等待付款"
          }
        }
      ]
    }
  ]
}
1.2.1 布局模板
代码语言:javascript复制
<!-- subpkg_consult/payment/index.vue -->
<script setup></script>

<template>
  <scroll-page>
    <view class="payment-page">
      <uni-section
        title-font-size="32rpx"
        title-color="#121826"
        padding="30rpx"
        title="图文问诊 49元"
      >
        <uni-list :border="false">
          <uni-list-item
            title="极速问诊"
            note="自动分配医生"
            thumb="/static/uploads/doctor-avatar.jpg"
            thumb-size="lg"
          />
          <uni-list-item title="优惠券" show-arrow right-text="-¥10.00" />
          <uni-list-item title="积分抵扣">
            <template #footer>
              <view class="uni-list-text-red">-¥1.00</view>
            </template>
          </uni-list-item>
          <uni-list-item title="实付款">
            <template #footer>
              <view class="uni-list-text-red">¥39.00</view>
            </template>
          </uni-list-item>
        </uni-list>
      </uni-section>

      <view class="dividing-line"></view>

      <uni-section
        title-font-size="32rpx"
        title-color="#121826"
        padding="30rpx"
        title="患者资料"
      >
        <uni-list :border="false">
          <uni-list-item title="患者信息">
            <template #footer>
              <view class="uni-list-text-gray"> 李富贵 | 男 | 30岁 </view>
            </template>
          </uni-list-item>
          <uni-list-item border title="病情描述" note="头痛,头晕,恶心" />
        </uni-list>
      </uni-section>

      <!-- <view class="payment-agreement">
        <radio color="#20c6b2" value="1" />
        我已同意<text style="color: #20c6b2">支付协议</text>
      </view> -->
    </view>
    <!-- 下一步操作 -->
    <view class="next-step">
      <view class="total-amount">
        合计: <text class="number">¥39.00</text>
      </view>
      <button class="uni-button">立即支付</button>
    </view>
  </scroll-page>
</template>

<style lang="scss">
  @import './index.scss';
</style>
代码语言:javascript复制
// subpkg_consult/payment/index.scss
.payment-page {
}

:deep(.uni-section-header) {
  font-weight: 500 !important;
  padding-left: 30rpx !important;
  padding-bottom: 0 !important;
}

:deep(.uni-section-content) {
  padding-top: 0 !important;
  padding-bottom: 0 !important;
}

:deep(.uni-list-item__container) {
  padding-left: 0 !important;
  padding-right: 0 !important;
}

:deep(.uni-list-item__content-title) {
  font-size: 32rpx !important;
  color: #3c3e42 !important;
}

:deep(.uni-list-item__extra-text) {
  font-size: 32rpx !important;
  color: #3c3e42 !important;
}

:deep(.uni-list-item__content-note) {
  font-size: 28rpx !important;
}

:deep(.uni-list-item__icon) {
  margin-right: 0 !important;
}

:deep(.uni-icon-wrapper) {
  padding: 0 !important;
  margin-right: -10rpx !important;
  font-size: 36rpx !important;
}

.dividing-line {
  height: 30rpx;
  background-color: #f6f6f6;
}

.uni-list-text-red {
  color: #eb5757;
}

.uni-list-text-gray {
  color: #848484;
  font-size: 30rpx;
}

.payment-agreement {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 180rpx;

  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 28rpx;

  :deep(.uni-radio-input) {
    transform: scale(0.9);
    margin-right: 0 !important;
  }

  /* #ifndef MP */
  radio {
    transform: scale(0.7);
    margin-right: -5rpx !important;
  }
  /* #endif */
}

.next-step {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;

  display: flex;
  align-items: center;
  height: 88rpx;
  padding: 30rpx 40rpx calc(env(safe-area-inset-bottom)   30rpx);
  background-color: #fff;

  .uni-button {
    width: 400rpx;
  }

  .total-amount {
    flex: 1;
    display: flex;
    align-items: center;
    font-size: 30rpx;
    color: #3c3e42;
  }

  .number {
    font-size: 40rpx;
    color: #eb5757;
    margin-left: 10rpx;
  }
}

在选择患者页面点【击一下】按钮时记录所选患者的 ID 并跳转到预付订单页面。

1.2.2 跳转页面

将所选择患者的 ID 记录到 Pinia 之中,然后跳转到预付订单详情页面

代码语言:javascript复制
// stores/consult.js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useConsultStore = defineStore(
  'consult',
  () => {
	
    // 省略前面小节的代码...
    
    // 患者ID
    const patientId = ref('')

    return { illnessInfo, initalValue, type, illnessType, depId, patientId }
  },
  {
    persist: {
      paths: ['illnessInfo', 'type', 'illnessType', 'depId', 'patientId'],
    },
  }
)

点击【下一步】时存储患者ID 并跳转页面

代码语言:javascript复制
<!-- subpkg_consult/patient/index.vue -->
<script setup>
  import { ref, computed } from 'vue'
  import { onShow } from '@dcloudio/uni-app'
  import { patientListApi } from '@/services/patient'
  
  import { useConsultStore } from '@/stores/consult'

  // 患者相关的数据
  const consultStore = useConsultStore()

  // 省略前面小节的代码...

  // 下一步操作
  function onNextStepClick() {
    // 将选中的患者ID记录到 Pinia 中
    consultStore.patientId = patientId.value
    // 下一步操作
    uni.navigateTo({ url: '/subpkg_consult/payment/index' })
  }

	// 省略前面小节的代码...
</script>
<template>
  <scroll-page>
   <!-- 省略前面小节的代码... -->
    <!-- 下一步操作 -->
    <view class="next-step">
      <button @click="onNextStepClick" class="uni-button">下一步</button>
    </view>
  </scroll-page>
</template>
1.2.3 订单信息

根据问诊类型 type 生成预支付订单并展示订单信息,在核对订单信息无误后方可进行支付。

  1. 先根据接口文档来封装调用接口的方法,接口文档地址在这里。
代码语言:javascript复制
// services/consult.js
import { http } from '@/utils/http'

// 省略前面小节的代码...

/**
 * 生成预支付订单
 */
export const preOrderApi = (type, options = {}) => {
  return http.get('/patient/consult/order/pre', {
    params: {
      type,
      ...options,
    },
  })
}
  1. 调用接口,生成预支付订单
代码语言:javascript复制
<!-- subpkg_consult/payment/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { useConsultStore } from '@/stores/consult'
  import { preOrderApi } from '@/services/consult'

  // 患者相关的数据(不具有响应性)
  const { type, illnessType } = useConsultStore()
  // 预付订单信息
  const preOrderInfo = ref({})

  // 生成预付订单
  async function createPreOrder() {
    // 预付订单信息
    const { code, data, message } = await preOrderApi(type, {
      illnessType,
    })
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.toast(message)
    // 渲染订单数据
    preOrderInfo.value = data
  }

  // 生成预支付订单
  createPreOrder()
</script>

<template>
  <scroll-page>
    <view class="payment-page">
      <uni-section
        title-font-size="32rpx"
        title-color="#121826"
        padding="30rpx"
        :title="`图文问诊 ${preOrderInfo.payment}元`"
      >
        <uni-list :border="false">
          <uni-list-item
            title="极速问诊"
            note="自动分配医生"
            thumb="/static/uploads/doctor-avatar.jpg"
            thumb-size="lg"
          />
          <uni-list-item
            title="优惠券"
            show-arrow
            :right-text="`-¥${preOrderInfo.couponDeduction}`"
          />
          <uni-list-item title="积分抵扣">
            <template #footer>
              <view class="uni-list-text-red">
                -¥{{ preOrderInfo.pointDeduction }}
              </view>
            </template>
          </uni-list-item>
          <uni-list-item title="实付款">
            <template #footer>
              <view class="uni-list-text-red">
                ¥{{ preOrderInfo.actualPayment }}
              </view>
            </template>
          </uni-list-item>
        </uni-list>
      </uni-section>
			<!-- 省略前面小节的代码... -->
    </view>
    <!-- 下一步操作 -->
    <view class="next-step">
      <view class="total-amount">
        合计: <text class="number">¥{{ preOrderInfo.actualPayment }}</text>
      </view>
      <button class="uni-button">立即支付</button>
    </view>
  </scroll-page>
</template>
1.2.4 患者信息

预付订单中除了展示订单信息外,还需要用户对患者信息进行核对。

代码语言:javascript复制
<!-- subpkg_consult/payment/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { useConsultStore } from '@/stores/consult'
  import { preOrderApi } from '@/services/consult'
  import { patientDetailApi } from '@/services/patient'

  // 患者相关的数据(不具有响应性)
  const { type, illnessType, patientId, illnessInfo } = useConsultStore()

  // 预付订单信息
  const preOrderInfo = ref({})
  // 就诊患者信息
  const patientDetail = ref({})

  // 省略前面小节的代码...

  // 获取患者信息
  async function getPatientDetail() {
    // 患者详情接口
    const { code, data, message } = await patientDetailApi(patientId)
    // 检测接口是否调用成功
    if (code !== 10000) return uni.utils.toast(message)
    // 渲染患者数据
    patientDetail.value = data
  }

  // 生成预支付订单
  createPreOrder()
  // 获取就诊患者信息
  getPatientDetail()
</script>

<template>
  <scroll-page>
    <view class="payment-page">
      <!-- 省略前面小节的代码 -->
      <!-- 患者资料 -->
      <uni-section
        title-font-size="32rpx"
        title-color="#121826"
        padding="30rpx"
        title="患者资料"
      >
        <uni-list :border="false">
          <uni-list-item title="患者信息">
            <template #footer>
              <view class="uni-list-text-gray">
                {{ patientDetail.name }} | {{ patientDetail.genderValue }} |
                {{ patientDetail.age }}岁
              </view>
            </template>
          </uni-list-item>
          <uni-list-item
            border
            title="病情描述"
            :note="illnessInfo.illnessDesc"
          />
        </uni-list>
      </uni-section>
    </view>
		<!-- 省略前面小节的代码 -->
  </scroll-page>
</template>

在上述代码中从 Pinia 获取数据时,采用的直接解构的方式获取的,这种方式解构出的数据是不具有响应式的,如果想要保持其响应式请使用 storeToRefs 后再进行解构。

1.3 待付订单

在核对预支付订单信息无误后,点击立即购买来生成待支付订单。

  1. 首先根据接口文档来封装接口调用的方法,接口文档的地址在这里。
代码语言:javascript复制
// services/consult.js
import { http } from '@/utils/http'

// 省略前面小节的代码...

/**
 * 生成待支付订单
 */
export const createOrderApi = (data) => {
  return http.post('/patient/consult/order', data)
}
  1. 调用接口生成待支付订单
代码语言:javascript复制
<!-- subpkg_consult/payment/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { useConsultStore } from '@/stores/consult'
  import { preOrderApi, createOrderApi } from '@/services/consult'
  import { patientDetailApi } from '@/services/patient'

  // 患者相关的数据(不具有响应性)
  const { type, illnessType, patientId, illnessInfo, depId } = useConsultStore()
	
  // 省略前面小节的代码...
  
  // 订单ID
  const orderId = ref('')

  // 立即支付
  async function onPaymentButtonClick() {
    if (orderId.value !== '') return uni.utils.toast('订单不能重复创建!')
    
    // 处理上传的图片,要求包含 ID 和 url (接口规订的)
    // 订单只能提交一次!!!
    illnessInfo.pictures = illnessInfo.pictures.map(({ url, uuid }) => {
      return { url, id: uuid }
    })
    
    // 生成订单接口
    const { code, data, message } = await createOrderApi({
      type,
      illnessType,
      depId,
      patientId,
      ...illnessInfo,
    })
    // 检测接口是否计用成功
    if (code !== 10000) return uni.utils.toast(message)
    
    // 接收订单ID
    orderId.value = data.id

    // 将 Pinia 中缓存的数据清空掉(订单已创建完成)
    const consultStore = useConsultStore()
    // 病情描述
    consultStore.illnessInfo = consultStore.initalValue
    consultStore.type = ''
    consultStore.illnessType = ''
    consultStore.depId = ''
    consultStore.patientId = ''

    // 选择支付渠道...
  }

  // 省略前面小节的代码...
</script>

<template>
  <scroll-page>
    <!-- 省略前面小节的代码... -->
     
    <!-- 下一步操作 -->
    <view class="next-step">
      <view class="total-amount">
        合计: <text class="number">¥{{ preOrderInfo.actualPayment }}</text>
      </view>
      <button @click="onPaymentButtonClick" class="uni-button">立即支付</button>
    </view>
  </scroll-page>
</template>
1.4 支付渠道

支付渠道指的完成支付的方式,最常见的有支付宝支付和微信支付,除此之外还有银联、百度钱包等。

1.4.1 custom-payment

根据需求的要求,在生成预付订单之后页面中需要弹出一个弹层,弹层中展示的内容为支付方式(渠道),由用户选择一种支付方式进行支付。

该弹层组件是以扩展组件 uni-popup 为核心的,关于 uni-popup 组件的使用文档请查看这里,这里只介绍我们用到的部分:

  • type 属性,指定弹层出现的位置
  • is-mask-click 是否允许点击蒙层关闭弹层
  • maskClick 点击弹层时触发事件
代码语言:javascript复制
<!-- pages/test/test.vue -->
<script setup>
  import { ref } from 'vue'
   
  // 省略前面小节的代码...

  // 弹层的引用
  const popupRef = ref()

  // 点击蒙层
  function onMaskClick() {
    console.log('蒙层点击了...')
  }
  
  // 打开弹层
  function openPopup() {
    popupRef.value.open()
  }

  // 关闭弹层
  function closePopup() {
    popupRef.value.close()
  }
</script>

<template>
  <scroll-page
    background-color="#f6f6f6"
    refresher-enabled
    @scrolltolower="test"
    @refresherrefresh="test"
  >
    <view class="content">
      <!-- 省略前面小节的代码... -->
			
      <view class="popup-demo">
        <button @click="openPopup" class="button" type="primary">
          打开弹层
        </button>
        <button @click="closePopup" class="button" type="primary">
          关闭弹层
        </button>
      </view>
      
      <uni-popup
        ref="popupRef"
        @maskClick="onMaskClick"
        :is-mask-click="false"
        type="bottom"
      >
        <view class="popup-container"></view>
      </uni-popup>

    </view>
  </scroll-page>
</template>

<style lang="scss">
  .content {
    padding: 30rpx 30rpx 0;
    overflow: hidden;
  }
  
  // 省略前面小节的代码...
  
  .popup-demo {
    display: flex;
    justify-content: space-between;
    margin: 30rpx 0;

    .button {
      width: 300rpx;
      margin: 0;
    }
  }

  .popup-container {
    height: 400rpx;
    background-color: #fff;
  }
</style>

在掌握了 uni-popup 的基本用法后,我们来封装支付渠道组件,组件要满足以下要求:

  1. 开放打开(open)和关闭(close)弹层的方法
代码语言:javascript复制
<!-- components/custom-payment/custom-payment.vue -->
<script setup>
  import { ref } from 'vue'
  // 在线支付弹层
  const paymentPopup = ref()
  // 打开弹层
  function open() {
    paymentPopup.value.open()
  }
  // 关闭弹层
  function close() {
    paymentPopup.value.close()
  }
	// 开放关闭和显示弹层的方法
  defineExpose({ open, close })
</script>
<template>
  <uni-popup :is-mask-click="false" ref="paymentPopup" type="bottom">
    <view class="payment-container">
      <view class="payment-header">
        <text class="title">选择支付方式</text>
        <uni-icons
          class="uni-icons-close"
          size="18"
          color="#333"
          type="closeempty"
        />
      </view>
      <view class="order-amount">¥ 99.9元</view>
      <uni-list :border="false">
        <uni-list-item
          title="支付宝支付"
          thumb="/static/images/alipay-icon.png"
        >
          <template #footer>
            <radio color="#16C2A3" />
          </template>
        </uni-list-item>
        <uni-list-item
          title="微信支付"
          thumb="/static/images/wechatpay-icon.png"
        >
          <template #footer>
						<uni-icons v-if="false" size="26" color="#16C2A3" type="checkbox-filled" />
						<uni-icons v-else size="26" color="#d1d1d1" type="circle" />
          </template>
        </uni-list-item>
      </uni-list>
      <button class="uni-button">立即支付</button>
    </view>
  </uni-popup>
</template>

<script>
  export default {
    options: {
      styleIsolation: 'shared',
    },
  }
</script>

<style lang="scss">
  .payment-container {
    min-height: 400rpx;
    border-radius: 30rpx 30rpx 0 0;
    background-color: #fff;
    padding: 10rpx 30rpx 40rpx;

    .payment-header {
      height: 88rpx;
      line-height: 88rpx;
      text-align: center;
      margin-bottom: 20rpx;
      font-size: 32rpx;
      color: #333;
      position: relative;
    }

    .uni-icons-close {
      position: absolute;
      top: 2rpx;
      right: 0;
    }

    .order-amount {
      padding: 10rpx 0 10rpx;
      text-align: center;
      font-size: 40rpx;
      color: #333;
    }

    :deep(.uni-list-item__container) {
      padding: 40rpx 0 !important;
    }
    
    :deep(.uni-list-item--hover) {
      background-color: #fff !important;
    }

    :deep(.uni-list-item__icon) {
      margin-right: 0;
    }

    .uni-button {
      margin-top: 40rpx;
    }
  }
</style>
  1. 支持两个自定义属性,orderIdamount
代码语言:javascript复制
<!-- components/custom-payment/custom-payment.vue -->
<script setup>
  import { ref } from 'vue'
  // 在线支付弹层
  const paymentPopup = ref()
  // 接收组件外部传入的数据
  const paymentProps = defineProps({
    // 待支付订单ID
    orderId: String,
    // 待支付金额
    amount: {
      type: [String, Number],
      default: 0,
    },
  })
	
  // 省略前面小节的代码...
</script>
<template>
  <uni-popup :is-mask-click="false" ref="paymentPopup" type="bottom">
    <view class="payment-container">
      <!-- 省略前面小节的代码... -->
      <view class="order-amount">¥ {{ paymentProps.amount }}</view>
      <!-- 省略前面小节的代码... -->
    </view>
  </uni-popup>
</template>
  1. 支持 3 个自定义事件,confirmchangeclose
  • 在用户切换选择支付方式时触发 change 事件
代码语言:javascript复制
<!-- components/custom-payment/custom-payment.vue -->
<script setup>
  import { ref } from 'vue'
  // 在线支付弹层
  const paymentPopup = ref()
  // 支付渠道的索引
  const channelIndex = ref(0)
  // 支付渠道(方式)
  const paymentChannel = [
    {
      title: '微信支付',
      thumb: '/static/images/wechatpay-icon.png',
    },
    {
      title: '支付宝支付',
      thumb: '/static/images/alipay-icon.png',
    },
  ]
  // 接收组件外部传入的数据
  const paymentProps = defineProps({
    // 待支付订单ID
    orderId: String,
    // 待支付金额
    amount: {
      type: [String, Number],
      default: 0,
    },
  })

  // 自定义事件
  const paymentEmits = defineEmits(['confirm', 'change', 'close'])
  
  // 切换支付渠道
  function onChannelChange(index) {
    // 当前选中渠道索引
    channelIndex.value = index
    // 触发 change 事件
    paymentEmits('change', { index })
  }

  // 省略前面小节的代码...
</script>
<template>
  <uni-popup :is-mask-click="false" ref="paymentPopup" type="bottom">
    <view class="payment-container">
      <!-- 省略前面小节的代码... -->
      <uni-list :border="false">
        <uni-list-item
          v-for="(channel, index) in paymentChannel"
          :key="channel.title"
          :title="channel.title"
          :thumb="channel.thumb"
          clickable
          @click="onChannelChange(index)"
        >
          <template #footer>
            <uni-icons
              v-if="channelIndex === index"
              size="26"
              color="#16C2A3"
              type="checkbox-filled"
            />
            <uni-icons v-else size="26" color="#d1d1d1" type="circle" />
          </template>
        </uni-list-item>
      </uni-list>
      <button class="uni-button">立即支付</button>
    </view>
  </uni-popup>
</template>
  • 在用户点击了弹层中的立即购买按钮后触发 confirm 事件
代码语言:javascript复制
<!-- components/custom-payment/custom-payment.vue -->
<script setup>
  // 省略前面小节的代码...
</script>
<template>
  <uni-popup :is-mask-click="false" ref="paymentPopup" type="bottom">
    <view class="payment-container">
      <!-- 省略前面小节的代码... -->
      <button
        @click="$emit('confirm', { index: channelIndex })"
        class="uni-button"
      >
        立即支付
      </button>
    </view>
  </uni-popup>
</template>
  • 在用户点击蒙层或者右上角关闭按钮时触发 close 事件
代码语言:javascript复制
<!-- components/custom-payment/custom-payment.vue -->
<script setup>
  // 省略前面小节的代码...
</script>
<template>
  <uni-popup
    @maskClick="$emit('close')"
    :is-mask-click="false"
    ref="paymentPopup"
    type="bottom"
  >
    <view class="payment-container">
      <view class="payment-header">
        <text class="title">选择支付方式</text>
        <uni-icons
          class="uni-icons-close"
          size="18"
          color="#333"
          type="closeempty"
          @click="$emit('close')"
        />
      </view>
      <!-- 省略前面小节的代码... -->
    </view>
  </uni-popup>
</template>

在待支付页面中当用户点击了蒙层或右上角关闭按钮时,调用 uni.showModal 弹出确认框:

代码语言:javascript复制
<!-- subpkg_consult/payment/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { useConsultStore } from '@/stores/consult'
  import { preOrderApi, createOrderApi } from '@/services/consult'
  import { patientDetailApi } from '@/services/patient'
	
  // 省略前面小节的代码...

  // 待支付订单ID
  const orderId = ref('')
  
  // 支付组件引用
  const paymentRef = ref()

  // 立即支付
  async function onPaymentButtonClick() {
    // 生成订单接口
    const { code, data, message } = await createOrderApi({
      type,
      illnessType,
      depId,
      patientId,
      ...illnessInfo,
    })
    // 检测接口是否计用成功
    if (code !== 10000) return uni.utils.toast(message)
    // 获取待支付订单ID
    orderId.value = data.id
    // 选择支付渠道
    paymentRef.value.open()
  }

  // 当支付弹层关闭时
  async function onPaymentClose() {
    const { confirm } = await uni.showModal({
      title: '关闭支付',
      content: '取消支付将无法获得医生回复,医生接诊名额有限,是否确认关闭?',
      cancelText: '仍要关闭',
      cancelColor: '#848484',
      confirmText: '继续支付',
      confirmColor: '#16C2A3',
    })

    if (!confirm) paymentRef.value.close()
  }
  
	// 省略前面小节的代码...
</script>

<template>
	<!-- 省略前面小节的代码... -->
  <!-- 支付渠道 -->
  <custom-payment
    @close="onPaymentClose"
    :amount="preOrderInfo.actualPayment"
    :order-id="orderId"
    ref="paymentRef"
  />
</template>
1.4.2 支付流程

一般的支付流程如下:

  1. 第三方支付提供的开发者平台注册账号、创建应用、申请认证用的证书或者 key
  2. 前端获取待支付订单ID、支付金额、支付渠道等数据,传递给后端接口
  3. 后端接口在获取前端传递的数据后,根据支付平台提供文档与支付平台接口进行对接
  4. 后端与支付平台对接成功后,后端将支付信息再回传给前端
  5. 前端根据回传的信息引导用户进行支付

在整个支付的过程中前端的任务仍然是调用接口(与调用普通的接口几乎没有差别),真正完成支付任务的其实是后端接口。

1.4.3 支付宝支付
  1. 自行注册支付宝支付账号
  2. 在企业中开发时需要创建应用,然而创建应用后还需要一些资质才可以进行支付,在课堂学习时无法满足这些资质,好在支付定平台提供了沙箱环境,沙箱环境是协助开发者进行接口开发及主要功能联调的模拟环境,目前仅支持网页/移动应用和小程序两种应用类型。
  3. 在正式应用或沙箱应用中获取到商家账号、认证证书、APPID、回调地址等。
  4. 上述的操作其实都是由后端来操作的,这里只是让大家了解一下支付管理后台的相关信息。

咱们的后端已经完成了与支付宝平台的支付对接,咱们前端只需要调用后端提供的接口即可。

  1. 根据接口文档封装调用接口的方法,接口文档的地址在这里。
代码语言:javascript复制
// services/payment.js
// 导入封装好的网络请求模块
import { http } from '@/utils/http'

/**
 * 三方支付(暂时只支持支付宝支付)
 */
export const orderPayApi = (data) => {
  return http.post('/patient/consult/pay', data)
}
  1. 调用支付接口
代码语言:javascript复制
<!-- subpkg_consult/payment/index.vue -->
<script setup>
  import { ref } from 'vue'
  import { useConsultStore } from '@/stores/consult'
  import { preOrderApi, createOrderApi } from '@/services/consult'
  import { patientDetailApi } from '@/services/patient'
  import { paymentApi } from '@/services/payment'

  // 患者相关的数据(不具有响应性)
  const { type, illnessType, patientId, illnessInfo, depId } = useConsultStore()

  // 省略前面小节的代码...

  // 支付
  async function onPaymentConfirm({ index }) {
    if (index === 0) return uni.utils.toast('暂不支持微信支付!')

    // 调用后端提供的支付接口
    const { code, data, message } = await paymentApi({
      orderId: orderId.value,
      paymentMethod: index,
      payCallback: 'http://localhost:5173/#/subpkg_consult/room/index',
    })

    // 接口是否调用成功
    if (code !== 10000) return uni.utils.toast(message)
    
    // #ifdef H5
    // 引导用户支付(地址跳转方式)
    window.location.href = data.payUrl
    // #endif

    // #ifdef MP-WEIXIN
    // 引导用户支付(wx.requestPayment 小程序)
    wx.requestPayment({
      // 4 个参数
    })
    // #endif
  }
	
  // 省略前面小节的代码...
</script>

<template>
  <scroll-page>
    <!-- 省略前面小节的代码... -->
    <!-- 下一步操作 -->
    <view class="next-step">
      <view class="total-amount">
        合计: <text class="number">¥{{ preOrderInfo.actualPayment }}</text>
      </view>
      <button @click="onPaymentButtonClick" class="uni-button">立即支付</button>
    </view>
  </scroll-page>
  <!-- 支付渠道 -->
  <custom-payment
    @close="onPaymentClose"
    @confirm="onPaymentConfirm"
    :amount="preOrderInfo.actualPayment"
    :order-id="orderId"
    ref="paymentRef"
  />
</template>

接口参数说明:

  1. orderId 待付订单的 ID,通过这个 ID 能查询到支付的金额
  2. paymentMethod 支付方式 0 微信支付、1 支付宝、2 云闪付
  3. payCallback 在支付完成后自动跳转的页面地址

沙箱应用账号,登录和支付密码都是 111111

askgxl8276@sandbox.com

scobys4865@sandbox.com

重点要注意的事项:

  • 目前只有支付宝提供了沙箱开发环境且只能支持H5的环境
  • 在调用支付接口成功后,根据回传会引导用户进行支付,但是引导的方式有多种
    • 在 H5 环境中是通过跳转即 window.location.hrefa 链接
    • 在小程序环境中要调用 wx.requestPayment
    • 在 PC 端可能提供一个二维码(本质还是一个链接)
    • 在 App 端调用 SDK 提供的方法

    但无论哪种方法都是要唤起手机上的支付宝 App 和微信 App 来进行支付。

1.4.4 微信支付

支付账号的申请受到限制且后端接口也暂未支持微信支付,暂不支持微信支付。

二、WebSocket

WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。你可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应,比较典型的应用场景就是即时通讯(聊天)系统。

2.1 原生用法
代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket 示例</title>
  </head>
  <body>
    <script>
      // 实例化 socket
      const ws = new WebSocket('wss://socketsbay.com/wss/v2/1/demo/')
      // 监听连接的建立
      ws.onopen = function (ev) {
        console.log('建立连接...')
        ws.send('Hello WebSockets!')
      }
      // 监听连接的断开
      ws.onclose = function (ev) {
        console.log('断开连接...')
      }
      // 监听 socket 服务器消息
      ws.onmessage = function (ev) {
        console.log('收到消息: '   ev.data)
      }
    </script>
  </body>
</html>

通过以上的示例大家只需要对 WebSocket 建立起这样在印象:

  • 采用的是 wss:// 协议
  • 分为客户端和服务端
  • 实现连续的、长时间的与服务器通信

然而 WebSocket 提供的只是通信底层的机制,结合业务通常需进行二次封装,其中比较流行就是 socket.io

2.2 Socket.IO库

Socket.IO 是基于 WebSocket 进行的二次封装,封装了更多的业务层面的逻辑,如身份认证等、事件驱动等。

注:其实 Socket.IO 不仅仅是封装了 WebSocket,还有基于 Ajax 的长轮询机制,在不支持 WebSocket 的环境中会自动降级为基于 Ajax 的长轮询机制。

长轮询机制可以粗暴的理解为在一个定时器中不断的重复发 Ajax 请求,以实现与服务器实时通信的功能。

  1. 下载 socket.io.js
代码语言:javascript复制
npm install socket.io

在项目中导入

代码语言:javascript复制
// ES modules
import { io } from "socket.io-client";

// CommonJS
const { io } = require("socket.io-client");

也可以使用 CDN 方式引入到项目当中:

代码语言:javascript复制
<script src="https://cdn.socket.io/4.4.1/socket.io.min.js"></script>
代码语言:javascript复制
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>socket.io 示例</title>
  </head>
  <body>
    <script src="https://cdn.socket.io/4.4.1/socket.io.min.js"></script>
    <script>
      // 建立连接
      const socket = io('http://localhost:3000', {
        transports: ['websocket', 'polling'],
      })
      // 监听连接建立状态
      socket.on('connect', () => {
        console.log('建立连接...')
      })
			// 监听连接断开状态
      socket.on('disconnect', () => {
        console.log('断开连接...')
      })
    </script>
  </body>
</html>

事件驱动,所谓的事件驱动是指服务端与客户端的通信过程是基于自定义事件来实现的:

  • on 方法用来监听一个事件,事件名随便定义
代码语言:javascript复制
// 建立连接
const socket = io('http://localhost:3000', {
  transports: ['websocket', 'polling'],
})

// 自定义事件,事件是用来传递数据的
socket.on('wait-some-data', (msg) => {
  console.log(msg)
})
  • emit 方法用来触发一个事件,这个事件名必须是服务端已经定义好的
代码语言:javascript复制
// 建立连接
const socket = io('http://localhost:3000', {
  transports: ['websocket', 'polling'],
})

// 自定义事件,事件是用来传递数据的
socket.on('wait-some-data', (msg) => {
  console.log(msg)
})

// 触发服务端定义的事件,将数据传递给服务端
socket.emit('send-some-data', '这里是数据', () => {
  // 
})

提供了 socket.io 服务端的示例大家,供大家学习 socket.io 的用法

代码语言:javascript复制
npm install socket.io
代码语言:javascript复制
// server.js
import { createServer } from 'http'
import { Server } from 'socket.io'
import path from 'path'

const server = createServer()
const io = new Server(server)

// websocket
io.on('connection', (client) => {
  console.log('建立连接...')
  
  // 消息发送
  client.on('sendToServer', (msg) => {
    console.log('收到了客户端的数据: '   msg)

    // 随机返回一条消息
    const messages = ['你好!', '我在写代码', '快下课了吧?']
    // 0 ~ 2 随机数据
    const index = Math.floor(Math.random() * 3)

    // 向客户端回复消息
    io.emit('sendToClient', messages[index])
  })

  // 断开连接
  client.on('disconnect', () => {
    console.log('断开连接...')
  })
})

server.listen(3000, () => {
  console.log('server start')
})

0 人点赞