Skip to content

uniCloud 与支付组件(三) ​

本页对应 uniCloud 与支付组件,共 8 段视频,约 1 小时 30 分。

学习目标 ​

  1. 能根据就诊信息中的 patientId 调用患者详情接口,获取并渲染患者姓名、性别、年龄,并从 illnessInfo 读取病情描述完成确认订单页展示。
  2. 能按接口文档组装并提交生成待付款订单参数,正确处理对象展开与参数层级,调用接口后接收并保存订单 ID。
  3. 能对 illnessInfo.pictures 使用 map 处理,将每张图片转换为仅含 id(UUID)和 url 的对象,并验证提交参数满足接口要求且订单创建成功。
  4. 能通过订单 ID 判断订单是否已创建,阻止重复提交并给出提示;创建成功后重置 consult 缓存数据,验证再次问诊时数据为全新状态。
  5. 能定位并修复 initialValue 直接赋值导致的浅拷贝污染,使用对象展开创建独立副本,验证两处数据修改互不影响。
  6. 能基于 uni-popup 封装支付渠道组件,使用 defineExpose 暴露 open/close 方法,定义并监听 close 自定义事件,从父组件完成弹层打开与关闭。

知识点 ​

患者详情:先拿患者 ID,再查详情 ​

为什么需要:确认订单页只展示了一部分数据,患者姓名、性别、年龄等还要从患者详情接口取。怎么想:查详情必须知道患者 ID;转写稿里这个 ID 已经存在 consult 状态中,所以先解构,再调前面封装好的 patientDetailAPI。怎么做:在 payment 页面从 consult 中取出 patientId,写 getPatientDetail,await patientDetailAPI(patientId),判断接口码不等于 10000 就 return,成功则把结果赋给 patient.value,并在页面加载时调用。易错点:如果 patientId 被做成响应式,传参要写 .value;不是响应式则直接传。转写稿里反复强调这一点,调试时先看网络请求里有没有带上正确患者 ID。

病情描述:不在患者详情里,要去 consult 状态取 ​

为什么需要:患者详情接口没有病情描述,但确认订单页要展示患者自己填写的描述。怎么想:描述在问诊流程中已经存进 consult,不是详情接口返回的。怎么做:从 consult 中取 illnessInfo,再取其中对应属性渲染。易错点:不要把“患者信息”与“病情描述”混成一个接口;描述是前面步骤缓存的,取错 store 就渲染不出来。

生成待付款订单:确认页之后还有“代付订单” ​

为什么需要:用户点“立即支付”并不是马上扣款,而是先生成待付款订单;用户取消支付后,订单仍处于待支付状态。怎么想:流程是选商品/选患者→确认预付订单→生成待付款订单→选择支付渠道→真正支付。怎么做:在 consult 相关 service 中封装 createOrderAPI,页面里写 createOrder,用 await 调接口,判断 code !== 10000 就退出。易错点:接口参数很多,转写稿说这些数据前面都已存进状态;传参时若解构对象,注意层级。调试时打开网络面板的“载荷/Payload”,转写稿中发现 type、desc 被包进了一个对象,而接口要求平级,于是用展开运算符 ... 展开。

过滤上传图片:接口要求图片含 id 和 url ​

为什么需要:生成订单接口对图片数据校验严格,图片对象必须包含 id 和 url;没有图片可以不传,有图片就必须合法。怎么想:前端上传后的图片对象里通常有 url 和 uuid,没有接口要的 id,所以提交前要映射一次。怎么做:对 illnessInfo.pictures 做 map,解构 url、uuid,返回 { url, id: uuid },覆盖原数组。转写稿中调试 500 错误时,后端提示缺少图片 ID;看 Payload 发现只有 url,于是用 UUID 充当 id。易错点:处理过的图片数据只能用一次;重复用同一份数据再提交,uuid 已被消耗,仍会 500。演示时要重新填病情描述、重新选图。

阻止订单重复创建:看订单 ID 是否存在 ​

为什么需要:订单创建成功后不能重复提交,否则用户会困惑,接口也可能报错。怎么想:后端创建成功会返回订单 ID,有 ID 就说明订单已生成。怎么做:定义 orderId,成功后赋值为返回数据中的 ID;再次点击时先判断 orderId.value 是否非空,非空则提示“订单不能重复创建”并 return。创建成功后还要清空 consult 中缓存的数据。易错点:清空时要用 initialValue,但转写稿发现 initialValue 被污染了——原因是某处把初始数据直接赋值给当前数据,二者指向同一内存。

浅拷贝 bug:对象赋值要复制一份 ​

为什么需要:直接 a = b 会让两个变量指向同一对象,改一个另一个也变,导致“清空数据”失效。怎么想:转写稿在 description 页面发现初始值和当前值同步变化,根源是直接赋值。怎么做:改成浅拷贝,例如给一个新对象再解构原值;转写稿明确说可用对象展开或 Object.assign 一类方式。易错点:只改清空逻辑没用,要改产生污染的赋值处;调试时对比两个对象是否仍相同。

支付渠道组件:用 uni-popup 封装,并暴露 open/close ​

为什么需要:支付方式弹层会在药品订单等多处复用,所以封装成全局组件。怎么想:弹层用 uni-popup,需要控制位置、是否点蒙层关闭、蒙层点击事件。怎么做:组件内定义 openPopup、closePopup,通过 defineExpose({ openPopup, closePopup }) 暴露;父页面用 paymentRef.value.openPopup() 打开。易错点:页面一加载就调子组件方法会报“方法不存在”,因为组件实例还没挂载;应放到按钮点击后调用。转写稿还设置 is-mask-click 为 false,点蒙层不关闭。

自定义事件:点叉号后由父组件关闭弹层 ​

为什么需要:蒙层不允许关闭后,叉号必须能关闭弹层。怎么想:叉号点击在组件内触发自定义事件,父组件监听后再调 closePopup。怎么做:组件内 defineEmits(['close']),叉号点击时 emit('close');父组件监听 @close,回调里执行 paymentRef.value.closePopup()。易错点:事件名要统一;父组件不监听就不会关闭。转写稿调试时先打印 111 确认事件触发,再替换为关闭逻辑。

1. 患者详情:先拿 patientId,再调详情接口 ​

为什么需要:确认订单页要展示患者姓名、性别、年龄,但页面本身只有 patientId,没有患者详情。
怎么想:前文做家庭档案时已经封装过 patientDetailAPI,这里不要重复造接口,直接从 consult store 取 patientId,再调接口。
怎么做:在 payment 页面解构 patientId,封装 getPatientDetail,成功后将结果写入 patient.value;病情描述不在患者详情里,要从 consult.illnessInfo 取。
易错点:如果 patientId 被做成响应式,传参要写 patientId.value;接口成功码按项目约定判断(转写中“二万/一万”是口误,实际以接口约定为准)。

js
const { patientId } = consultStore
const patient = ref({})

async function getPatientDetail() {
  const res = await patientDetailAPI(patientId)
  if (res.code !== 200) return message.error(res.message)
  patient.value = res.data
}

2. 生成待付款订单:确认页之后不是立刻支付 ​

为什么需要:用户点“立即支付”后,如果直接跳支付,用户取消就没有订单记录;实际流程应是“预付订单确认 → 生成待付款订单 → 支付 → 已支付”。
怎么想:确认页只是让用户核对商品/就诊信息;点支付时先调后端生成待付款订单,拿到订单 ID 后再进入支付渠道选择。
怎么做:在 consult service 下封装 createOrderAPI,页面中写 createOrder 方法,把前序存在 store 里的参数传给接口。
易错点:接口参数有必填和可选,type、depId、病情描述、图片等要平铺到正确层级;不能把 type 塞进 illnessInfo 里面。

js
async function createOrder() {
  const res = await createOrderAPI({
    type,
    depId,
    ...illnessInfo
  })
  if (res.code !== 200) return message.error(res.message)
  orderId.value = res.data.id
}

3. 过滤上传图片:接口要求 id 和 url ​

为什么需要:生成订单接口对图片校验较严:没有图片可以不传,但有图片就必须包含 id 和 url,否则 500。
怎么想:上传组件给的数据里通常有 uuid 和 url,但接口要的是 id。提交前遍历 pictures,把 uuid 映射成 id。
怎么做:用 map 生成新数组,返回 { id: uuid, url },再覆盖原 pictures。
易错点:处理后的数据会被“消费”,同一份图片数据重复提交可能失败;调试时最好重新生成一份患者/病情数据。

js
illnessInfo.pictures = illnessInfo.pictures.map(item => {
  const { uuid, url } = item
  return { id: uuid, url }
})

4. 阻止订单重复创建:用 orderId 做闸门 ​

为什么需要:订单只能提交一次,用户重复点“立即支付”不能重复生成订单。
怎么想:接口成功后会返回订单 ID,有 ID 就说明订单已创建;再次点击时直接拦截并提示。
怎么做:定义 orderId = ref(''),成功后赋值;点击方法开头判断 if (orderId.value) return message.error('订单不能重复创建')。
易错点:只拦截还不够,订单创建完成后应清空缓存数据,避免下一次问诊复用旧数据;清空时注意浅拷贝 bug。

5. 浅拷贝 bug:清空缓存为什么没生效 ​

为什么需要:创建订单后要清空 consult 中的病情数据,但直接赋值会让“初始值”和“当前值”指向同一对象。
怎么想:const initialValue = illnessInfo 不是拷贝,改一个另一个也变;清空时把 initialValue 赋回当前值,等于没清空。
怎么做:在 description 页面初始化时用展开运算符做浅拷贝:const initialValue = { ...illnessInfo }。
易错点:对象赋值要区分“引用”和“拷贝”;浅拷贝只适合当前这层字段,深层对象仍需按需处理。

6. 支付渠道组件:用 uni-popup 封装全局弹层 ​

为什么需要:生成待付款订单后要弹出支付方式选择,且药品订单等地方也会复用,所以封装成全局组件。
怎么想:弹层用 uni-popup,type="bottom" 从底部出现;不允许点蒙层关闭,点叉叉才关闭。
怎么做:创建 CustomPayment 组件,在 test 页面加载;组件内用 defineExpose 暴露 openPopup、closePopup,父组件通过 ref 调用。
易错点:新建全局组件后最好重启项目;页面加载时立刻调 ref 可能组件尚未挂载,放到按钮点击事件里更稳。

js
defineExpose({ openPopup, closePopup })
// 父组件
paymentRef.value?.openPopup()

7. 自定义事件:叉叉关闭交给父组件处理 ​

为什么需要:子组件点叉叉时不能直接改父组件状态,应通过事件通知父组件。
怎么想:子组件 defineEmits(['close']),点击叉叉 emit('close');父组件监听 @close,在回调里调用子组件暴露的 closePopup。
怎么做:<CustomPayment ref="paymentRef" @close="handleClose" />,handleClose 中执行 paymentRef.value.closePopup()。
易错点:defineExpose 暴露方法、defineEmits 定义事件要分清;事件回调里再关闭弹层,逻辑更清晰。

先复现问题:为什么传了图片就 500 ​

为什么需要:生成待付款订单时,图片参数是可选参数。没有图片可以正常创建订单;一旦传了图片,接口就会校验图片数据,要求每个图片项必须包含 id 和 url。缺一个就可能返回 500。

怎么想:先不要急着改代码,先用两组数据对比:一组不上传图片,一组上传图片。转写稿里的调试过程就是这样:不传图片时订单能成功;上传两张图片后,再点“立即支付”,接口返回 500。此时要看响应信息和 Network 里的 Payload,确认前端到底传了什么。

怎么做:打开接口文档,确认图片字段要求;再看请求载荷。错误信息会提示缺少图片 id。检查 Payload 可发现,图片项里只有 url,没有 id。

易错点:不要只看“500”就猜是接口坏了;也不要只看前端控制台。要同时看接口返回、请求载荷和接口文档。转写稿里“fe的T个ID”应理解为“缺少图片 ID”。

处理思路:把上传图片映射成接口需要的结构 ​

为什么需要:上传组件返回的图片数据里通常有 uuid 和 url,但接口要的是 id 和 url。前端不能直接把原对象丢给后端,必须在提交前做一次数据加工。

怎么想:图片数据在 illnessInfo.pictures 里,它是一个数组。数组里每一项是一个对象,可以用 map 遍历,把每项解构出 url 和 uuid,再返回新对象 { url, id: uuid }。也就是用 uuid 充当接口需要的 id。

怎么做:在生成订单、调用接口前处理。按项目里的响应式写法补 .value;如果 illnessInfo 是普通对象则去掉 .value。关键代码可写成:

js
illnessInfo.value.pictures = illnessInfo.value.pictures.map(({ url, uuid }) => {
  console.log(url, uuid)
  return {
    url,
    id: uuid
  }
})

处理后再提交给创建订单接口。转写稿里强调:map 要 return 一个新数组,新数组会覆盖原来的图片数组;每一项最终只保留接口需要的 url 和 id。

易错点:第一,map 必须返回新对象,不能只写 console.log;第二,解构字段名要对应上传数据,转写稿里是 uuid,不是 id;第三,处理过一次后原数据里的 uuid 可能已被消费,不能反复拿旧数据测试。

提交与验证:看载荷、看订单 ID ​

为什么需要:处理完图片数据后,还要确认提交结构正确。否则虽然前端“看起来处理了”,后端仍可能拿不到 id。

怎么想:先打印处理前后的图片数据,确认每项都有 url 和 id;再点“立即支付”,在 Network 的 Payload 里检查 pictures 数组。接口成功后,返回结果里会出现订单 ID,这个 ID 表示待付款订单已经创建成功。

怎么做:调用创建订单方法,成功后接收订单 ID。转写稿中的思路是:接口成功就取返回数据里的订单 ID 保存起来;后续支付会用到它。

易错点:转写稿里反复提醒:图片数据只能用一次。第一次提交成功后,如果再用同一份数据点第二次,可能又报 500,因为 uuid 已被处理/消费。要重新走一遍问诊流程,重新选患者、填病情描述、上传图片,生成全新的图片数据再测。调试时如果发现数据“被污染”,不要怀疑接口,先重新生成数据。

本节小结 ​

本节的核心不是“把图片过滤掉”,而是“把上传图片加工成接口能接受的结构”:接口要求有图片时必须带 id 和 url;前端从 illnessInfo.pictures 中取出 url 和 uuid,通过 map 映射为 { url, id: uuid },再随订单参数提交。调试顺序是:复现 500 → 看接口文档 → 看 Payload → 补 id → 再提交验证。最关键的易错点是:map 要返回新数组、字段名要对、处理过的图片数据不能反复使用。

代码示例 ​

1. 预付订单 —— 查询患者详情并渲染病情描述 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>预付订单 - 患者信息</title>
  <style>
    body { margin: 0; padding: 16px; font-size: 14px; background: #f6f7fb; }
    .card { background: #fff; border-radius: 8px; padding: 12px 16px; }
    .row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
    .row:last-child { border-bottom: none; }
    .label { color: #666; }
    .value { color: #333; font-weight: 500; }
  </style>
</head>
<body>
  <div id="app">
    <div class="card">
      <div class="row"><span class="label">患者姓名</span><span class="value">{{ patient.name }}</span></div>
      <div class="row"><span class="label">性别</span><span class="value">{{ patient.genderValue }}</span></div>
      <div class="row"><span class="label">年龄</span><span class="value">{{ patient.age }}</span></div>
      <div class="row"><span class="label">病情描述</span><span class="value">{{ illnessInfo.illnessDesc }}</span></div>
    </div>
  </div>

  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script>
    const { createApp, ref, onMounted } = Vue

    // 真实项目:import { patientDetailAPI } from '@/services/patient'
    // 此处用 Promise 模拟 patient/detail 接口的返回
    const patientDetailAPI = (id) =>
      new Promise((resolve) => {
        setTimeout(() => {
          resolve({
            code: 10000,
            message: 'success',
            data: { id, name: '张三', genderValue: '男', age: 32 }
          })
        }, 300)
      })

    createApp({
      setup() {
        // 真实项目:
        // const consultStore = useConsultStore()
        // const { patientId, illnessInfo } = storeToRefs(consultStore)
        const patientId = ref('1234567890')
        const illnessInfo = ref({ illnessDesc: '咳嗽三天,夜间加重' })

        const patient = ref({})

        // 患者详情
        const getPatientDetail = async () => {
          const res = await patientDetailAPI(patientId.value)
          if (res.code !== 10000) {
            alert(res.message)
            return
          }
          patient.value = res.data
        }

        onMounted(() => getPatientDetail())

        return { patient, illnessInfo }
      }
    }).mount('#app')
  </script>
</body>
</html>

要点 / 易错点

  • patientId 是从 consult store 里解构出来的(storeToRefs),所以调用接口时传 patientId.value;如果没做成响应式就直接传 patientId,这一点要分清楚。
  • 接口只返回患者姓名、性别、年龄,不含病情描述。病情描述是用户在病情描述页自己填的,已存进 consult store 的 illnessInfo 里,这里直接从 store 取,不要去接口里找。
  • code !== 10000 后面必须 return 中断,否则会继续执行 patient.value = res.data,把 undefined 赋进去,页面直接白屏。

2. 生成待付款订单 —— 过滤上传的图片数据 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>生成待付款订单</title>
  <style>
    body { margin: 0; padding: 16px; font-size: 14px; background: #f6f7fb; }
    .card { background: #fff; border-radius: 8px; padding: 12px 16px; }
    pre { background: #f5f5f5; padding: 8px; border-radius: 6px; overflow: auto; }
    button { width: 100%; padding: 12px; background: #16c79a; color: #fff; border: none; border-radius: 8px; }
  </style>
</head>
<body>
  <div id="app">
    <div class="card">
      <div>患者ID:{{ patientId }}</div>
      <div>病情描述:{{ illnessInfo.illnessDesc }}</div>
      <div>图片数量:{{ illnessInfo.pictures.length }}</div>
      <pre>{{ JSON.stringify(illnessInfo.pictures, null, 2) }}</pre>
      <button @click="createOrder">立即支付</button>
    </div>
  </div>

  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script>
    const { createApp, ref } = Vue

    // 真实项目:import { createOrderAPI } from '@/services/consult'
    // 接口约定:pictures 每一项必须包含 id 和 url,缺一个就报错
    const createOrderAPI = (params) =>
      new Promise((resolve) => {
        const illegal = (params.pictures || []).some((p) => !p.id || !p.url)
        if (illegal) return resolve({ code: 500, message: '图片缺少 id' })
        resolve({ code: 10000, message: 'success', data: { id: 'ORDER_' + Date.now() } })
      })

    createApp({
      setup() {
        const patientId = ref('1234567890')
        const depId = ref('dep-001')
        const illnessType = ref(1)

        // 后端返回的原始图片:只有 url / uuid / path,没有 id
        const illnessInfo = ref({
          illnessDesc: '咳嗽三天,夜间加重',
          pictures: [
            { url: 'https://xxx/a.png', uuid: 'u-001', path: 'a.png' },
            { url: 'https://xxx/b.png', uuid: 'u-002', path: 'b.png' }
          ]
        })

        // 过滤上传的图片数据:接口要求每项必须包含 id 和 url
        const filterPictures = () => {
          illnessInfo.value.pictures = illnessInfo.value.pictures.map((item) => {
            const { url, uuid } = item
            return { url, id: uuid }
          })
        }

        const createOrder = async () => {
          filterPictures()

          const res = await createOrderAPI({
            patientId: patientId.value,
            depId: depId.value,
            illnessType: illnessType.value,
            ...illnessInfo.value // 展开,让 illnessDesc / pictures 与其它参数平级
          })

          if (res.code !== 10000) {
            alert(res.message)
            return
          }
          alert('订单创建成功:' + res.data.id)
        }

        return { patientId, illnessInfo, createOrder }
      }
    }).mount('#app')
  </script>
</body>
</html>

要点 / 易错点

  • map 返回的是新数组,再赋值回 illnessInfo.value.pictures 覆盖旧数组;不要写成 const arr = ... 就完事,原数据不会变。
  • 后端给的是 uuid,接口要的是 id,所以 return { url, id: uuid } 做重命名;{ url } 是简写,等价于 url: url。
  • 传参时 ...illnessInfo.value 必须展开。不展开的话 illnessDesc、pictures 会嵌在 illnessInfo 里,而后端要的是平级字段,直接报 500(载荷里能看到参数层级不对)。
  • 过滤有副作用:uuid 被取走之后,再点第二次按钮,uuid 已经没了,id 就取不到 → 这个数据只能提交一次。测试时每次都要从头重新填一遍数据,否则会莫名其妙失败。

3. 数据浅拷贝 bug 修复 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>浅拷贝 bug 修复</title>
  <style>
    body { margin: 0; padding: 20px; font-family: system-ui, sans-serif; font-size: 14px; }
    .box { border: 1px solid #ddd; border-radius: 8px; padding: 12px; margin-bottom: 16px; }
    .line { margin-bottom: 8px; }
    input { padding: 4px 8px; }
    button { margin-left: 8px; padding: 4px 12px; }
    b { color: #e5533d; }
  </style>
</head>
<body>
  <h3>① 直接赋值(有 bug)</h3>
  <div class="box">
    <div class="line">初始值 init1.illnessDesc = <b id="init1">(空)</b></div>
    <div class="line">当前值 illnessInfo1.illnessDesc = <b id="cur1">(空)</b></div>
    <input id="input1" placeholder="输入病情描述" />
    <button id="reset1">重置为初始值</button>
  </div>

  <h3>② 浅拷贝(修复)</h3>
  <div class="box">
    <div class="line">初始值 init2.illnessDesc = <b id="init2">(空)</b></div>
    <div class="line">当前值 illnessInfo2.illnessDesc = <b id="cur2">(空)</b></div>
    <input id="input2" placeholder="输入病情描述" />
    <button id="reset2">重置为初始值</button>
  </div>

  <script>
    /* ---------- ① 直接赋值:两个变量指向同一块内存 ---------- */
    const init1 = { illnessDesc: '' }
    let illnessInfo1 = init1 // 问题就在这一行

    const input1 = document.getElementById('input1')
    const reset1 = document.getElementById('reset1')

    input1.addEventListener('input', () => {
      illnessInfo1.illnessDesc = input1.value
      render1()
    })

    reset1.addEventListener('click', () => {
      illnessInfo1 = init1 // 重置:但 init1 早被污染了
      input1.value = illnessInfo1.illnessDesc
      render1()
    })

    function render1() {
      document.getElementById('init1').textContent = init1.illnessDesc || '(空)'
      document.getElementById('cur1').textContent = illnessInfo1.illnessDesc || '(空)'
    }

    /* ---------- ② 浅拷贝:每次都拿到一个全新的对象 ---------- */
    const init2 = { illnessDesc: '' }
    let illnessInfo2 = { ...init2 } // 关键:展开运算符 = 浅拷贝

    const input2 = document.getElementById('input2')
    const reset2 = document.getElementById('reset2')

    input2.addEventListener('input', () => {
      illnessInfo2.illnessDesc = input2.value
      render2()
    })

    reset2.addEventListener('click', () => {
      illnessInfo2 = { ...init2 } // 重新浅拷贝一份
      input2.value = ''
      render2()
    })

    function render2() {
      document.getElementById('init2').textContent = init2.illnessDesc || '(空)'
      document.getElementById('cur2').textContent = illnessInfo2.illnessDesc || '(空)'
    }

    render1()
    render2()
  </script>
</body>
</html>

要点 / 易错点

  • 对象用 = 赋值是引用传递:let a = b 之后两个变量指向同一块内存,改 a.xxx 会同时改掉 b.xxx。这就是视频里“初始值被污染”的根源。
  • 视频中的 bug 出在病情描述页:illnessInfo.value = initialIllnessInfo,用户一输入就把 initialIllnessInfo 也改了;等订单创建成功后用 initialIllnessInfo 去清空缓存,数据根本清不掉。
  • 修复只需一行:illnessInfo.value = { ...initialIllnessInfo },用展开运算符生成新对象。重置时同理,要重新 { ...init } 一次。
  • 浅拷贝只复制第一层。illnessInfo 里如果还嵌套了 pictures 数组,数组仍是同一个引用,需要深拷贝时得另想办法。

常见错误 ​

  1. 接口调用时参数为空

    • 现象:患者详情、生成订单接口都报参数缺失,payload 里 patientId、illnessType 是 undefined。
    • 原因:从 Pinia 解构时有的写成响应式(storeToRefs),有的直接解构,调用时 .value 加与不加没有统一。
    • 修法:先定规矩——解构出来的是 ref 就一律 .value,是普通值就直接用;同一文件内保持一种写法,不要混用。
  2. 生成待付款订单报 500,提示缺少图片 id

    • 现象:不传图片时下单成功,一上传图片点“立即支付”就 500,pictures 里只有 url 没有 id。
    • 原因:接口约定 pictures 每项必须同时含 id 与 url,前端把本地图片对象原样透传了。
    • 修法:提交前 map 一遍,return { id: uuid(), url },用新数组覆盖旧数组;注意图片数据只能用一次,重复测试要从头走一遍问诊流程。
  3. 参数层级错了,字段被套进子对象

    • 现象:后端说缺 type、desc,但载荷里明明有,只是它们跑到了 pictures 内部。
    • 原因:把 illnessInfo 当成一个整体字段传,没有展开。
    • 修法:用展开运算符把对象平铺到最外层,习惯上把 ... 放在对象末尾,避免覆盖同名参数。
  4. 重复点击“立即支付”报错

    • 现象:第一次下单成功,再点一次报 500;或用户完全不知道订单已经建过。
    • 原因:没有记录订单是否已创建,也没有拦截重复提交。
    • 修法:用一个 orderId 的 ref 存后端返回的订单号;进入提交逻辑先判空,有值就提示“订单不能重复创建”并 return;创建成功后把缓存数据整体重置。
  5. 重置数据“失效”,病情描述内容还在

    • 现象:订单创建完、清空了 illnessInfo 等字段,再次进入描述页旧内容仍然出现。
    • 原因:initialValue = illnessInfo 是引用赋值,两个变量指向同一块内存,改一个另一个跟着变,重置等于把数据又赋回给自己。
    • 修法:改成浅拷贝 initialValue = { ...illnessInfo }(或 Object.assign({}, illnessInfo));同时留意 pictures 数组仍是共享引用,必要时一并拷贝。
  6. 调用组件暴露的方法报 undefined

    • 现象:paymentRef.value.openPopup() 提示读取不到 open 或 value 为空。
    • 原因:三种情况——组件没写 defineExpose;模板没绑 ref;在 onMounted 里就调用,此时组件实例还没挂载完。
    • 修法:子组件 defineExpose({ openPopup, closePopup });父页面模板绑 ref;调用放到用户点击的回调里,而不是页面加载时。
  7. 弹层点叉叉关不掉 / 点蒙层误关闭

    • 现象:点关闭图标没反应;或手一滑点到蒙层,支付方式选择框直接消失。
    • 原因:子组件只 emit('close'),父页面没监听;蒙层默认可点关闭。
    • 修法:子组件 emit 事件,父页面 @close 绑回调里调 closePopup();同时给弹层加 :is-mask-click="false",只保留叉叉作为唯一关闭入口。

练习 ​

  1. 在确认订单页补全患者信息:从 store 取 patientId 调 patientDetailAPI,把姓名、性别、年龄渲染到模板;病情描述从 illnessInfo 中取,图片按数组循环渲染。注意响应式取值的写法要统一。

  2. 实现 createOrder:参数从 store 取齐后平铺传给接口,pictures 做 map 过滤成 { id, url };接口失败 return,成功后把订单号存进 orderId。用 console.log 打印过滤前后的数据对比一次。

  3. 给“立即支付”加防重:已存在 orderId 时提示并中断;创建成功后重置 illnessInfo、type、depId、patientId。写完后从填写描述页开始完整跑两遍,验证第二遍是干净的新数据。

  4. 修复 initialValue 的引用污染 bug:先复现(改描述页内容,看 initialValue 是否同步变化),再改成浅拷贝,再复现一次确认隔离。同时思考:pictures 数组被 map 覆盖后,initialValue 里那份会不会受影响?

  5. 封装支付渠道组件:uni-popup 从底部弹出、蒙层不可点关闭、叉叉触发 close 事件;组件 defineExpose 出打开/关闭两个方法。父页面用 ref 调用打开,用 @close 回调关闭。写完后验证“打开—关闭—再打开”循环是否正常。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1优医咨询-预付订单患者信息06:31
2优医咨询-生成待付款订单15:58
3优医咨询-过滤上传的图片数据13:47
4优医咨询-阻止订单重复创建11:03
5优医咨询-数据浅拷贝bug修复07:57
6优医咨询-支付渠道组件07:29
7优医咨询-支付组件暴露方法17:22
8优医咨询-支付组件自定义事件10:34

小结 ​

  1. 患者信息与病情描述来自两个地方:患者详情要拿 patientId 单独查,病情描述早在问诊填写时就存进了 store,直接取即可——先想清楚数据在哪,再动手写请求。
  2. 支付流程是“预付订单确认 → 生成待付款订单 → 真正支付”三段,中间那段用户往往感知不到,但它才是落库、拿到订单号的关键一步。
  3. 生成订单的参数是一整包散装字段,全部来自 store;对象参数要平铺而不是嵌套,用展开运算符时放在末尾最稳妥。
  4. 上传图片必须先过滤:接口只认 { id, url },id 用 uuid 生成;同时要接受“图片数据只能用一次”这个现实,调试时反复失败多半是数据被消费过了。
  5. 订单的“已创建”状态用订单号来判定,用 ref 存住它做重复提交拦截;订单一旦建成,缓存数据就该整体清空,否则下一个患者会带着上一个人的信息。
  6. 引用赋值是这一节最隐蔽的坑:两个变量指向同一块内存,重置会变成自我赋值。{ ...obj } 这种浅拷贝是最小成本的修法,但要记得它只解决第一层。
  7. 组件封装三件套要形成肌肉记忆:defineProps 收数据、defineEmits 抛事件、defineExpose 露方法;暴露的方法只能在组件挂载之后调用,所以放在交互回调里而不是生命周期里。
  8. 弹层组件的打开与关闭都应由外部驱动:子组件只管显示和抛事件,父页面决定何时开、何时关,这样同一个组件才能在问诊订单和药品订单两处复用。