主题
uniCloud 与支付组件(三)
本页对应 uniCloud 与支付组件,共 8 段视频,约 1 小时 30 分。
学习目标
- 能根据就诊信息中的 patientId 调用患者详情接口,获取并渲染患者姓名、性别、年龄,并从 illnessInfo 读取病情描述完成确认订单页展示。
- 能按接口文档组装并提交生成待付款订单参数,正确处理对象展开与参数层级,调用接口后接收并保存订单 ID。
- 能对 illnessInfo.pictures 使用 map 处理,将每张图片转换为仅含 id(UUID)和 url 的对象,并验证提交参数满足接口要求且订单创建成功。
- 能通过订单 ID 判断订单是否已创建,阻止重复提交并给出提示;创建成功后重置 consult 缓存数据,验证再次问诊时数据为全新状态。
- 能定位并修复 initialValue 直接赋值导致的浅拷贝污染,使用对象展开创建独立副本,验证两处数据修改互不影响。
- 能基于 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数组,数组仍是同一个引用,需要深拷贝时得另想办法。
常见错误
接口调用时参数为空
- 现象:患者详情、生成订单接口都报参数缺失,
payload里patientId、illnessType是undefined。 - 原因:从 Pinia 解构时有的写成响应式(
storeToRefs),有的直接解构,调用时.value加与不加没有统一。 - 修法:先定规矩——解构出来的是
ref就一律.value,是普通值就直接用;同一文件内保持一种写法,不要混用。
- 现象:患者详情、生成订单接口都报参数缺失,
生成待付款订单报 500,提示缺少图片 id
- 现象:不传图片时下单成功,一上传图片点“立即支付”就 500,
pictures里只有url没有id。 - 原因:接口约定
pictures每项必须同时含id与url,前端把本地图片对象原样透传了。 - 修法:提交前
map一遍,return { id: uuid(), url },用新数组覆盖旧数组;注意图片数据只能用一次,重复测试要从头走一遍问诊流程。
- 现象:不传图片时下单成功,一上传图片点“立即支付”就 500,
参数层级错了,字段被套进子对象
- 现象:后端说缺
type、desc,但载荷里明明有,只是它们跑到了pictures内部。 - 原因:把
illnessInfo当成一个整体字段传,没有展开。 - 修法:用展开运算符把对象平铺到最外层,习惯上把
...放在对象末尾,避免覆盖同名参数。
- 现象:后端说缺
重复点击“立即支付”报错
- 现象:第一次下单成功,再点一次报 500;或用户完全不知道订单已经建过。
- 原因:没有记录订单是否已创建,也没有拦截重复提交。
- 修法:用一个
orderId的ref存后端返回的订单号;进入提交逻辑先判空,有值就提示“订单不能重复创建”并return;创建成功后把缓存数据整体重置。
重置数据“失效”,病情描述内容还在
- 现象:订单创建完、清空了
illnessInfo等字段,再次进入描述页旧内容仍然出现。 - 原因:
initialValue = illnessInfo是引用赋值,两个变量指向同一块内存,改一个另一个跟着变,重置等于把数据又赋回给自己。 - 修法:改成浅拷贝
initialValue = { ...illnessInfo }(或Object.assign({}, illnessInfo));同时留意pictures数组仍是共享引用,必要时一并拷贝。
- 现象:订单创建完、清空了
调用组件暴露的方法报 undefined
- 现象:
paymentRef.value.openPopup()提示读取不到open或value为空。 - 原因:三种情况——组件没写
defineExpose;模板没绑ref;在onMounted里就调用,此时组件实例还没挂载完。 - 修法:子组件
defineExpose({ openPopup, closePopup });父页面模板绑ref;调用放到用户点击的回调里,而不是页面加载时。
- 现象:
弹层点叉叉关不掉 / 点蒙层误关闭
- 现象:点关闭图标没反应;或手一滑点到蒙层,支付方式选择框直接消失。
- 原因:子组件只
emit('close'),父页面没监听;蒙层默认可点关闭。 - 修法:子组件
emit事件,父页面@close绑回调里调closePopup();同时给弹层加:is-mask-click="false",只保留叉叉作为唯一关闭入口。
练习
在确认订单页补全患者信息:从 store 取
patientId调patientDetailAPI,把姓名、性别、年龄渲染到模板;病情描述从illnessInfo中取,图片按数组循环渲染。注意响应式取值的写法要统一。实现
createOrder:参数从 store 取齐后平铺传给接口,pictures做map过滤成{ id, url };接口失败return,成功后把订单号存进orderId。用console.log打印过滤前后的数据对比一次。给“立即支付”加防重:已存在
orderId时提示并中断;创建成功后重置illnessInfo、type、depId、patientId。写完后从填写描述页开始完整跑两遍,验证第二遍是干净的新数据。修复
initialValue的引用污染 bug:先复现(改描述页内容,看initialValue是否同步变化),再改成浅拷贝,再复现一次确认隔离。同时思考:pictures数组被map覆盖后,initialValue里那份会不会受影响?封装支付渠道组件:
uni-popup从底部弹出、蒙层不可点关闭、叉叉触发close事件;组件defineExpose出打开/关闭两个方法。父页面用ref调用打开,用@close回调关闭。写完后验证“打开—关闭—再打开”循环是否正常。
课堂画面






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