主题
uniCloud 与支付组件(一)
本页对应 uniCloud 与支付组件,共 8 段视频,约 1 小时 20 分。
学习目标
- 能解释 uniCloud/云开发与服务端、运维之间的关系,能获取 uni-app 项目 AppID,并按规定先关联云空间再启动项目,说明未关联或顺序错误时会出现的问题。
- 能使用 uni-file-picker 组件配置上传数量、文件类型、图片预览样式和 v-model,将病情图片上传至云空间,并通过进度条、v-model 返回地址及云存储管理后台验证上传成功。
- 能基于病情描述、患病时长、是否就诊三个字段编写计算属性,控制“下一步”按钮的 disabled 状态,并通过逐项填写与删除数据验证按钮启用/禁用逻辑。
- 能使用 Pinia 定义问诊相关 store,区分初始空数据与患者病情数据,使用 storeToRefs 解构保持响应式,完成病情描述数据的持久化缓存并通过刷新页面验证回显。
- 能使用 uni.showModal 在存在缓存数据时提示用户恢复数据,并根据确认/取消结果保留或清空数据,通过清除本地缓存验证弹窗显示条件。
- 能通过点击事件获取地址中的 type、pho、deptId 参数并存入 Pinia 与本地缓存,完成跳转到患者选择页面,并验证地址参数、Pinia 状态和本地存储中的数据一致。
知识点
先理解:uniCloud 不是“玄学”
为什么需要:很多人一听“云开发”就觉得高级,其实它本质是把服务端、运维中繁琐的服务器配置简化掉,让前端也能写原本后端要写的代码。怎么想:把云空间理解成“已经帮你管好的服务器”,你只管用,不必自己买服务器、配域名、远程开机。怎么做:在 uni-app 里开通并关联 uniCloud 服务空间;开发测试可用免费/开发者版,公司运营按实际情况用付费版。易错点:不要把云开发和“神奇新技术”混为一谈;免费版主要用于开发测试,真正运营通常要付费;开发者版可续期,但要以控制台实际显示为准。
获取 APPID:别拿小程序 APPID 混用
为什么需要:uni-app 项目有自己的应用标识,后续关联云空间、管理项目都要用到。怎么想:先区分“小程序 APPID”和“uni-app 项目 APPID”,名字都叫 APPID,但不是一回事。怎么做:打开项目里的 manifest.json,在“基础配置”里查看“uni-app 应用标识”;没有就点“获取”,有就不用重复获取。也可到管理后台“账号管理—应用管理—我的应用”核对。易错点:把小程序 APPID 填到这里;已有 APPID 还反复点“重新获取”。
启动顺序:先关联云空间,再启动项目
为什么需要:项目启动时若还没关联云空间,后续上传图片、云函数等能力不会生效。怎么想:把“关联空间”当成每天打开项目后的第一步。怎么做:打开项目后先关联 uniCloud 服务空间,再运行项目;如果忘了,停掉项目,重新关联后再启动。易错点:先启动再关联,控制台提示“未关联空间”;以为刷新页面就能补救,实际要按顺序重启。
上传图片:用 uni-file-picker
为什么需要:病情描述要上传患处图片,图片不能只存在浏览器内存里,要传到云空间。怎么想:云空间就是服务器,上传组件负责把文件传到已关联的云空间;用 v-model 接上传结果,成功后可拿到图片地址。怎么做:使用扩展组件 uni-file-picker,限制数量、类型和预览样式:
html
<uni-file-picker
v-model="pic"
:limit="8"
file-extname="png,jpg,gif"
:image-styles="{ width: '80px', height: '80px' }"
/>上传成功后,pic 中会出现 path 或 url,这就是云存储地址。易错点:只看页面出现图片就以为成功,必须检查 v-model 是否真有数据;上传时没有进度条,大概率是项目没关联云空间;file-extname 写错会导致类型限制不生效。
查看云存储:后台能管理上传结果
为什么需要:要知道图片到底有没有进服务器,以及后续能否删除管理。怎么想:控制台里的“云存储”就是你的文件管理后台。怎么做:进入 uniCloud 控制台,点“云存储/cloud storage”,可看到刚上传的图片,也可删除。易错点:把“本地预览”当成“云端已保存”;上传成功应以 v-model 数据和云存储列表为准。
数据验证:用 disabled + computed
为什么需要:病情描述、患病时长、是否就诊过没填完时,“下一步”不应可点;图片允许不上传。怎么想:按钮是否禁用,取决于三个必填值是否都不为空。怎么做:
js
const isEnabled = computed(() => {
return desc.value !== '' &&
duration.value !== '' &&
flag.value !== ''
})按钮上写 :disabled="!isEnabled"。易错点:三个判断少写显式比较,结果可能变成 0/1 而不是布尔值;忘记取反,导致“填完反而禁用”;把图片也设成必填。
病情数据缓存:Pinia + storeToRefs
为什么需要:用户填完病情后刷新或返回,数据不应凭空消失;再次进入时可提示“是否恢复之前填写的数据”。怎么想:页面刷新后变量内存会丢,所以要持久化到本地;用 Pinia 管理问诊数据,并只持久化需要跨页面/刷新保留的字段。怎么做:在 consult store 中定义 info、initialValue、type、consultId、depId,持久化 info 等;页面中不能直接解构 store,否则丢响应式,要用 storeToRefs:
js
const store = useConsultStore()
const { info, initialValue } = storeToRefs(store)易错点:直接解构导致页面不更新;把 initialValue 也持久化,取消时无法清空;无缓存也弹恢复提示。
恢复提示与清空:uni.showModal
为什么需要:自动回填会让用户觉得“莫名其妙”,应让用户选择是否恢复。怎么想:有缓存才弹窗;确认则保留,取消则用 initialValue 覆盖清空。怎么做:
js
uni.showModal({
title: '提示',
content: '是否恢复之前填写的数据',
confirmText: '确认',
cancelText: '取消',
success(res) {
if (!res.confirm) {
info.value = { ...initialValue.value }
}
}
})易错点:没有缓存也弹;取消时只改某个子字段,导致清不干净;忘记 confirm 为 true、取消为 false 的判断。
地址参数缓存:按钮跳转时存 Pinia
为什么需要:从首页到快速问诊可用链接拼参数,但当前“下一步”是按钮,继续拼长地址很麻烦,且这些参数要跨页面使用。怎么想:链接场景拼地址省事;按钮场景监听点击事件,把地址参数存 Pinia 再跳转更省事。怎么做:用 defineProps 接收地址参数,点击“下一步”时存入 store 并跳转:
js
const props = defineProps({
type: String,
consultId: String,
depId: String
})
store.type = props.type
store.consultId = props.consultId
store.depId = props.depId
uni.navigateTo({ url: '/pages/patient/index' })易错点:参数名与地址中的名字不一致;先跳转再存导致下页取不到;store 中没定义对应字段;需要跨页面的字段忘记持久化。
本节目标与云空间认知
本节要实现“病情描述上传图片”,并顺带完成数据验证、图片云存储查看、病情数据缓存和地址参数缓存。先明确:uniCloud 云开发不是神秘新技术,本质是把服务器、运维、后端的一部分工作简化了。为什么需要?因为上传图片必须有服务端存储;怎么想?把云空间当成已配置好的服务器;怎么做?先在 HBuilderX 中关联服务空间,再启动项目;易错点:顺序必须是“先关联、再启动”。转写中反复调试发现:若启动时未关联,运行后会提示未关联空间;此时应停项目、关联后重启。另外要在 manifest.json 的基础配置中获取 uni-app 项目 AppID,注意它不等同于小程序 AppID。
(若原笔记无图,此处仅保留说明,不编造链接。)
上传图片组件:uni-file-picker
为什么需要?病情描述要允许患者上传患处图片;怎么想?使用扩展组件 uni-file-picker,它可把文件上传到已关联的云空间;怎么做?在页面中放入组件,配置 v-model 接收上传结果,limit 限制张数,file-extname 限制扩展名,image-styles 调整预览宽高和边框。示例:
vue
<uni-file-picker
v-model="pic"
:limit="8"
file-extname="png,jpg,jpeg"
:image-styles="{ width: '80px', height: '80px' }"
/>js
const pic = ref([]) // 病情图片,字段名按后端接口命名为 pic易错点:组件本身会自动上传到已关联云空间,但必须用 v-model 接收结果;上传成功后可看到 pic 中多出对象,里面有 path 或 url,即云存储地址。转写中调试:若选择图片后出现蓝色进度条,说明云空间关联生效;若只显示图片却没有进度条,大概率是项目未关联云空间。上传后还可在 uniCloud 后台“云存储 / cloud storage”中查看和管理图片。
病情数据验证与按钮禁用
为什么需要?避免用户未填关键信息就点“下一步”;怎么想?文字描述、患病时长、是否就诊过三项不能为空,图片可不传;怎么做?给按钮加 :disabled,用计算属性判断三项是否都有值:
js
const isEnabled = computed(() => {
return !!consult.desc && !!consult.duration && !!consult.flag
})vue
<button :disabled="!isEnabled">下一步</button>易错点:转写中一开始计算属性返回了 0/1,原因是布尔项没有参与判断,需三项都判断,保证结果为布尔值;disabled 为真时按钮禁用,因此绑定时要取反。样式上可用属性选择器 [disabled] 给禁用按钮置灰。
病情数据缓存:Pinia 持久化与确认框
为什么需要?用户离开或刷新后,之前填写的数据应可恢复;怎么想?数据不能只放页面内存,要放 Pinia 并持久化到本地;怎么做?在 consult store 中定义 info 和 initialValue,只持久化 info;页面通过 storeToRefs 解构以保持响应式:
js
import { storeToRefs } from 'pinia'
const consultStore = useConsultStore()
const { info } = storeToRefs(consultStore)页面加载时用 uni.showModal 询问是否恢复:
js
uni.showModal({
title: '提示',
content: '是否恢复之前填写的数据?',
confirmText: '确认',
cancelText: '取消',
confirmColor: '#00aa00',
success(res) {
if (res.cancel) {
consultStore.info = consultStore.initialValue
}
}
})易错点:没有缓存时不要弹窗,应先判断本地是否有数据;点“取消”时用 initialValue 覆盖当前数据即可清空。转写中验证:确认后保留数据,取消后表单和图片清空。
地址参数缓存到 Pinia
为什么需要?后续提交不仅需要病情描述,还需要类型、科室 ID 等,跨多个页面传递;怎么想?链接跳转时拼参数方便,但到按钮跳转时,把参数存入 Pinia 更方便;怎么做?在“下一步”点击事件中读取地址参数,存入 store,再 uni.navigateTo 到患者选择页。
易错点:读取参数名必须与地址中的参数名一致;需要跨页使用的 type、consultId、deptId 要持久化,而仅当前页使用的 info 不必持久化。转写中验证:点击下一步后,Pinia 与本地存储中都能看到这三个参数,页面也正常跳到 patient 页。
本节要解决的问题
病情描述页有三个关键数据:文字描述、患病时长、是否就诊过。转写稿里明确:图片可以不上传,但前三个数据必须都有值;只要有一个为空,“下一步”按钮就应该是灰色不可点的。
为什么需要:如果按钮一直可点,用户漏填也能进入下一页,后面提交时才发现数据不全,体验和校验都会变差。
怎么想:按钮是否禁用,不直接写死,而是由一个“计算属性”根据三个字段实时算出来。
怎么做:模板里给按钮绑定 :disabled,计算属性返回布尔值。
易错点:不要把 disabled 写死;图片不参与必填校验。
按钮禁用与样式
转写稿中按钮本身是 button,加了 disabled 后点击事件也不生效。为了让用户看得出“禁用”,又用属性选择器给带 disabled 的按钮加了灰色样式。
html
<button
class="uni-button"
:disabled="!canNext"
@click="onNextStepClick"
>
下一步
</button>css
.uni-button[disabled] {
color: #999;
background: #eee;
}为什么需要:只逻辑禁用、不视觉禁用,用户会反复点。
怎么想:disabled 控制能不能点,属性选择器控制看起来能不能点。
怎么做::disabled="!canNext",canNext 为真时按钮可用。
易错点:属性选择器要写在按钮类名后面;不要只改颜色而忘了 disabled 属性。
计算属性的推导与调试
转写稿里的推导是:描述不为空、患病时长不为空、是否就诊不为空,三个条件同时成立才允许下一步。调试时先刷新页面,三个值都为空,按钮应为灰;逐项填写,只有第三项也填完按钮才变亮。
js
import { computed } from 'vue'
const canNext = computed(() => {
return Boolean(form.desc) &&
Boolean(form.duration) &&
Boolean(form.flag)
})转写稿里出现了一个小瑕疵:结果变成了 0/1,不是纯布尔值。原因是布尔字段参与 && 时没有显式判断。用 Boolean(...) 或显式比较,保证最终返回 true/false。
为什么需要:按钮状态必须随三个字段实时变化。
怎么想:三个条件用逻辑与连接,任何一个不满足就禁用。
怎么做:计算属性返回布尔值,模板取反后交给 disabled。
易错点:ref 数据在 JS 里要 .value;字段名要和后端接口一致;图片字段不要加进必填条件。
上传图片与云存储验证
图片虽可不传,但上传成功与否要能确认。转写稿用 uni-file-picker,通过 v-model 拿上传结果,limit 限制张数,file-extname 限制 png/jpg 等,imageStyles 改预览宽高和边框。
html
<uni-file-picker
v-model="form.pic"
limit="8"
file-extname="png,jpg,jpeg"
:imageStyles="{ width: '80px', height: '80px' }"
/>调试时看两点:上传时是否有进度条;v-model 对应数据里是否出现 path/url。转写稿强调:没有进度条,大概率是项目没有先关联云空间再启动。
易错点:先关联 uniCloud 空间,再启动项目;上传后可在云存储后台查看和删除图片。
数据缓存与恢复提示(关联步骤)
转写稿还补了缓存:用户再次进入页面时,用 showModal 询问是否恢复上次填写的数据;点取消时用 initialValue 把数据清空。Pinia 中定义 initialValue 和 info,只持久化需要跨页面/刷新保留的数据;解构要用 storeToRefs 保持响应式。
js
uni.showModal({
title: '提示',
content: '是否恢复之前填写的数据?',
success(res) {
if (!res.confirm) {
consult.info = { ...consult.initialValue }
}
}
})易错点:没有缓存也弹窗会吓到用户,应先判断本地是否有缓存;直接解构 store 会丢响应式。
地址参数缓存与跳转(关联步骤)
点“下一步”时,把地址里的 type、科室 ID 等参数取出来存进 Pinia,再跳转下一页,避免地址越拼越长。参数名必须和地址中的参数名一致,先存再跳。
js
const type = options.type
const depId = options.depId
consult.type = type
consult.depId = depId
uni.navigateTo({ url: '/patient/index' })小结
本节核心是“三个必填字段控制按钮禁用”:用计算属性算布尔值,用 :disabled 控制点击,用样式给出视觉反馈;图片可选,但要验证上传结果。缓存和地址参数是后续联动步骤,调试时重点看布尔值、字段名和云空间关联顺序。
代码示例
示例一:病情描述上传图片(uni-file-picker → uniCloud 云存储)
vue
<template>
<view class="desc-page">
<view class="title">病情描述</view>
<textarea
v-model="info.desc"
class="desc-input"
placeholder="请描述您的病情(必填)"
maxlength="200"
/>
<!-- 只要项目已关联 uniCloud 空间,这个组件就会自动把文件传到云存储 -->
<uni-file-picker
v-model="info.pic"
:limit="8"
file-extname="png,jpg,gif"
:image-styles="{ width: '60px', height: '60px', border: 'none' }"
@select="onSelect"
@progress="onProgress"
@success="onSuccess"
@delete="onDelete"
/>
</view>
</template>
<script setup>
import { ref } from 'vue'
// pic 用来接收上传成功后的结果:数组,元素里有 path / url
const info = ref({
desc: '',
pic: []
})
const onSelect = (e) => {
console.log('选中文件', e.tempFilePaths)
}
const onProgress = (e) => {
console.log('上传进度', e.progress) // 上传大图时会一直变化
}
const onSuccess = (e) => {
console.log('上传成功', e)
}
const onDelete = (e) => {
console.log('删除', e)
}
</script>
<style>
.desc-input {
width: 100%;
height: 200rpx;
background-color: #f5f5f5;
}
</style>要点解释 / 易错点
v-model="info.pic":uni-file-picker 支持 v-model,它有两个作用——上传成功后把结果(含path、url)写回数组,以及回显已上传的图片。v-model 必须写完整,只写一半会直接报错。file-extname="png,jpg,gif":限定可上传的后缀(extension);limit="8":最多 8 张。:image-styles:改的是预览图的宽高、边框,跟上传逻辑无关,只是 UI。- 最关键的易错点:组件之所以能上传,是因为项目已经关联了 uniCloud 服务空间。判断是否关联成功的小技巧——选择图片后有没有蓝色进度条;有进度条=已关联,没进度条但图片出来了=大概率没关联。另外打开项目要先关联空间、再启动项目,顺序反了关联不生效,只能停掉重跑。
- 上传后到「云空间 → 云存储(cloud storage)」里能看到文件,那里也可以直接删除。
示例二:病情描述数据验证(计算属性控制按钮 disabled)
vue
<template>
<view class="desc-page">
<textarea v-model="info.desc" placeholder="请描述您的病情(必填)" />
<radio-group v-model="info.duration">
<label><radio value="一周内" />一周内</label>
<label><radio value="一个月内" />一个月内</label>
<label><radio value="半年内" />半年内</label>
</radio-group>
<radio-group v-model="info.flag">
<label><radio :value="true" />就诊过</label>
<label><radio :value="false" />未就诊</label>
</radio-group>
<!-- 图片不限制,可以不上传 -->
<button class="uni-button" :disabled="!canNext" @click="onNextStep">
下一步
</button>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
const info = ref({
desc: '', // 病情描述
duration: '', // 患病时长
flag: null, // 是否就诊过
pic: [] // 图片(允许为空)
})
// 三个必填项都有值,才允许点击下一步
const canNext = computed(() => {
const { desc, duration, flag } = info.value
return desc !== '' && duration !== '' && flag !== null && flag !== ''
})
const onNextStep = () => {
uni.navigateTo({ url: '/pages/patient/index' })
}
</script>
<style>
/* 属性选择器:按钮带 disabled 属性时才生效 */
.uni-button[disabled] {
background-color: #cccccc !important;
color: #ffffff !important;
}
</style>要点解释 / 易错点
:disabled="!canNext":canNext为 true(三项都填了)→!canNext为 false → 不渲染 disabled 属性,按钮可点;反之变灰。注意要取反,因为 disabled 是「禁用」语义。- 按钮本身要写
:disabled才有禁用效果,只写@click是不管用的;[disabled]是属性选择器,用来给禁用态换颜色。 - 三个条件都必须写显式判断。易错点:
flag是「是否就诊过」,false也是有效值,如果直接写desc && duration && flag,用户选了「未就诊」就会被判成没填。所以必须写成flag !== null && flag !== ''。 - 另一个易错点:
radio-group的 value 容易被转成字符串或 0/1,导致布尔类型判断失效;出现「值不是布尔」时先检查是不是少了某个!== ''的判断。 - 图片
pic不参与校验——业务上允许不上传。
示例三:病情数据缓存(pinia + 持久化 + showModal 恢复数据)
js
// stores/consult.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useConsultStore = defineStore('consult', () => {
// 1. 空数据模板:点「取消」时用它整体覆盖,实现一键清空
const initialValue = ref({
desc: '',
duration: '',
flag: null,
pic: []
})
// 2. 页面真正使用的病情数据,初始就是空模板
const info = ref({ ...initialValue.value })
// 3. 地址参数,跨页面使用,也放进 store
const type = ref('')
const depId = ref('')
const docId = ref('')
return { initialValue, info, type, depId, docId }
}, {
// 只持久化这里列出的数据;initialValue 只是模板,不用缓存
persist: {
paths: ['info', 'type', 'depId', 'docId']
}
})vue
<!-- pages/description/index.vue -->
<script setup>
import { storeToRefs } from 'pinia'
import { useConsultStore } from '@/stores/consult'
const consult = useConsultStore()
// 必须用 storeToRefs 解构,直接解构会丢响应式
const { info } = storeToRefs(consult)
// 页面一进来就判断:本地到底有没有缓存过数据
const hasCache =
info.value.desc !== '' || info.value.duration !== '' || info.value.flag !== null
if (hasCache) {
uni.showModal({
title: '提示',
content: '是否恢复之前填写的数据?',
confirmText: '确认',
cancelText: '取消',
confirmColor: '#00b96b',
success: (res) => {
if (!res.confirm) {
// 点取消:用空模板覆盖,数据就清空了
info.value = { ...consult.initialValue }
}
}
})
}
</script>要点解释 / 易错点
- 页面刷新、甚至退回首页再进来,数据还在,说明数据不能放组件内存,必须落到本地存储;用 pinia 的
persist指定paths,等于「点谁谁缓存」。 - 最容易踩的坑:
const { info } = consult解构后不再是响应式数据,页面不会跟着变,必须storeToRefs(consult)再解构。 - 两个数据分工:
initialValue是空模板(只用来清空,不缓存),info才是页面 v-model 绑定的真数据(要缓存)。名字保持一致,页面代码就不用改。 uni.showModal:res.confirm为 true 是「确认」,false 是「取消」;confirmText/cancelText/confirmColor可自定义文案和颜色。- 必须加
hasCache判断:本地根本没缓存还弹窗,用户会更懵;只有缓存里真有数据才提示。点「取消」时才执行info.value = { ...consult.initialValue }把数据清空。 - 地址参数(type / depId / docId)也一起存进 store 并持久化,是为了避免在页面间不断拼长地址;但「下一步」这类按钮场景要在点击事件里先
deffiend取参、再赋值进 store,最后才navigateTo跳转。
常见错误
- 打开项目后直接运行,云空间不生效 → 未先关联 uniCloud 空间就启动项目 → 先关联云空间,再重启/启动项目。
- 把小程序 APPID 当成 uni-app 项目 APPID 使用 → 两个 APPID 概念混淆 → 在
manifest.json基础配置中获取当前 uni-app 项目 APPID。 - 图片看似上传成功,但后续取不到数据 → 只看页面预览,未检查
v-model结果或上传进度条 → 同时验证v-model中是否有path/url,并观察上传进度条。 - 按钮禁用逻辑出现
0/1而非布尔值 → 计算属性中省略了某个布尔判断 → 三个必填条件都显式比较,确保最终结果为布尔值。 - 从 Pinia 解构后页面不随数据变化 → 直接解构导致响应式丢失 → 使用
storeToRefs后再解构。 - 一进页面就弹恢复缓存提示 → 未判断本地是否真有缓存 → 先读本地缓存,有缓存才调用
showModal。 - 点“取消”后旧数据仍回显 → 未用初始空数据覆盖当前数据 → 用
initialValue重置对应状态。 - 点“下一步”后地址参数没进 Pinia → 未在点击事件中取值,或参数名与地址不一致 → 在
onNextStep中按地址参数名获取并写入 store,再跳转。
练习
- 复现“未关联云空间就启动项目”的场景,记录报错现象,并按正确顺序修复。
- 上传一张图片,分别通过页面预览、
v-model数据、云存储管理后台三处确认上传结果。 - 实现病情描述页按钮禁用逻辑,要求三个必填项都填写后才可点击,并验证结果为布尔值。
- 实现“恢复之前填写的数据”弹窗:有缓存才提示,确认则保留,取消则清空。
- 从上一页地址参数中取出
type、科室、医生 ID,存入 Pinia 并跳转下一页,刷新后验证持久化结果。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | uniCloud空间介绍 | 10:36 |
| 2 | 优医咨询-病情描述上传图片 | 11:12 |
| 3 | 优医咨询-病情描述数据验证 | 10:48 |
| 4 | 优医咨询-查看云存储的图片 | 02:06 |
| 5 | 优医咨询-病情数据缓存-1 | 12:23 |
| 6 | 优医咨询-病情数据缓存-2 | 09:55 |
| 7 | 优医咨义昌-病情数据缓存-3 | 10:12 |
| 8 | 优医咨询-地址参数缓存 | 13:12 |
小结
- 云空间使用前先关联,再启动项目,顺序不能反。
- uni-app 项目 APPID 与小程序 APPID 要区分。
- 图片上传成功要以
v-model数据和上传进度为依据,不能只看预览。 - 按钮是否禁用应由计算属性统一控制,并保证布尔结果。
- Pinia 解构取值要使用
storeToRefs,避免响应式丢失。 - 缓存恢复提示必须先判断本地是否存在缓存。
- 取消恢复缓存时,用初始空数据覆盖当前数据完成清空。
- 跨页面参数可放 Pinia,点击“下一步”时写入 store 并跳转。