主题
组件通信与表单组件(四)
本页对应 组件通信与表单组件,共 7 段视频,约 57 分钟。
学习目标
- 能够分别使用选项式和组合式语法定义 Pinia Store,并正确映射 state、getters、actions 到 ref、computed、普通函数。
- 能够定义并导出 Pinia 中的数据与方法,在组件中调用方法修改状态,并正确处理 ref 数据的
.value。 - 能够使用 computed 基于索引值派生当前登录方式数据,并实现 0/1 索引切换及对应文案更新。
- 能够创建登录页面并引入样式,完成密码登录与验证码登录两个表单组件的拆分、局部导入和条件渲染。
- 能够通过调试工具检查 Pinia Store 与组件状态,验证数据、计算属性和切换交互是否符合预期。
知识点
先建立映射:选项式像 Vuex,组合式是 setup 写法
为什么需要:Pinia 的功能和 Vuex 类似,但写法有两套。直接死记语法容易混,最稳的办法是先建立映射:选项式里的 state/getters/actions,在组合式里分别对应 ref/computed/普通函数。
怎么想:转写稿反复强调“原来的 state 现在用 ref 定义”“getters 就是 computed”“定义函数直接写 function”。所以不要把它们当成三套新东西,而是 Vuex 经验到 Pinia 的迁移。
怎么做:选项式用对象:
js
defineStore('counter', {
state: () => ({ count: 0, name: '小明' }),
getters: {
double: (state) => state.count * 2
},
actions: {
increment() { this.count++ },
decrement() { this.count-- }
}
})组合式用 setup 函数:
js
defineStore('counter', () => {
const count = ref(0)
const name = ref('小明')
const double = computed(() => count.value * 2)
function increment() { count.value++ }
function decrement() { count.value-- }
return { count, name, double, increment, decrement }
})易错点:组合式里定义的数据、函数、计算属性都要 return;改 ref 必须 .value。转写稿里点加号没反应,就是因为 increment/decrement 没有 return,Devtools 报“不是函数”。
state:选项式 state,组合式 ref
为什么需要:页面要显示的数据必须先进入 store,否则模板拿不到响应式数据。
怎么想:选项式里 state 相当于 Vuex 的 state;组合式里没有 state 选项,就用 ref 声明。转写稿说“使用 ref 定义的数据,相当于 Vuex 中的 state”。
怎么做:选项式:
js
state: () => ({ count: 0, name: '小明' })组合式:
js
const count = ref(0)
const name = ref('小明')
return { count, name }模板里用 store.count、store.name。转写稿中把 count 改成 10 后页面变成 10,说明结果一样,只是语法不同。
易错点:组合式里不要忘记 return { count, name };defineStore 的第一个参数是 store 名字,转写稿口述为 counter,这个名字会显示在调试插件里,不要乱起。
方法:选项式 actions,组合式普通函数
为什么需要:点加号、点减号要改变数据,就需要方法。
怎么想:选项式沿用 Vuex 思路,把方法写进 actions,里面可以用 this.count++;组合式里事件回调就是普通函数,直接改 count.value。
怎么做:选项式:
js
actions: {
increment() { this.count++ },
decrement() { this.count-- }
}组合式:
js
function increment() { count.value++ }
function decrement() { count.value-- }
return { count, increment, decrement }模板中写 @click="store.increment" 或 @click="store.decrement"。
易错点:组合式里 count 是 ref,必须 count.value++;函数也要 return,否则模板调用会报错。转写稿中手动刷新后恢复正常,就是因为补了 return。
计算属性:选项式 getters,组合式 computed
为什么需要:根据一个数据生成另一个数据,例如 double = count * 2。
怎么想:Vuex 的 getters 在 Pinia 选项式里仍是 getters;组合式里用 computed。转写稿说“getters 就是 computed,只是换了名字”。
怎么做:选项式:
js
getters: {
double: (state) => state.count * 2
}组合式:
js
const double = computed(() => count.value * 2)
return { double }模板用 store.double。转写稿中 count 是 10 时 double 是 20,点加号后两者一起变。
易错点:组合式 computed 里要写 count.value;计算属性也要 return;模板里 ref 会自动解包,不要写 store.count.value。
登录页与 tab 数据
为什么需要:登录页要支持密码登录和验证码登录,页面文字和表单要随切换变化。
怎么想:转写稿先定义数组,数组第 0 项和第 1 项各放一对标题/副标题;再用 tabIndex 记录当前索引,用计算属性 tabMeta 取出当前项。这样只切换索引,就能换文字。
怎么做:
js
const tabIndex = ref(0)
const tabMeta = computed(() => tabList[tabIndex.value])模板中标题用 tabMeta.title,副标题用 tabMeta.subTitle。转写稿在 Devtools 的 Components 里查看 tabIndex=0、tabMeta 是否取到第 0 项,确认后再渲染。
易错点:tabIndex 是 ref,计算属性里要 .value;tabMeta 不是 ref,模板里不要再点 .value;数组名按你页面中已定义的登录方式数组来写。
tab 切换:0/1 互转
为什么需要:点击文字要密码登录/验证码登录来回切。
怎么想:取值只有 0 和 1。转写稿推导:1 - 1 = 0,0 - 1 = -1,取绝对值又变 1,所以用 Math.abs(tabIndex.value - 1) 可实现 0/1 互转。
怎么做:
js
function onSubTitleClick() {
tabIndex.value = Math.abs(tabIndex.value - 1)
}模板:
html
<span @click="onSubTitleClick">{{ tabMeta.subTitle }}</span>易错点:要写 tabIndex.value = ... 重新赋值;转写稿调试时先看 Devtools 中索引是否 0/1 切换,再看计算属性和文字是否跟着变。不要忘记 Math.abs 的 Math。
表单组件拆分与 v-if
为什么需要:密码登录和验证码登录是两个表单,只在本页面使用,拆成局部组件更清晰。
怎么想:页面下建 components 目录,放 password.vue、mobile.vue 和公共样式文件。只在本页用的组件,不按 easycom 全局规范,直接导入;导入组件不加花括号。
怎么做:
js
import CustomPassword from './components/password.vue'
import CustomMobile from './components/mobile.vue'模板用 v-if 只显示一个:
html
<CustomPassword v-if="tabIndex === 0" />
<CustomMobile v-if="tabIndex === 1" />易错点:组件没导入页面就不出现;v-if 条件别写反;scoped 的作用是样式隔离,转写稿口述“共享”要纠正为“避免样式污染”,父组件要影响子组件样式需用深度选择器,不是 scoped 本身实现共享。
从需求出发:为什么要在 Pinia 里“定义方法”
这一节的核心需求很直接:页面上有加号、减号,点击后让数字 count 发生变化。转写稿里先把它和 Vuex 对照:Vuex 里通常用 actions 或 mutations 定义函数;到了 Pinia,仍然要解决“点击时执行一段操作、修改 store 里的数据”这件事。
所以思路是:先不急着记语法,而是把 Pinia 的写法和 Vuex 建立映射。选项式写法里用 actions 放方法;组合式写法里,事件回调就是一个普通函数。最终目标一致:页面点击 → 调用 store 方法 → 修改响应式数据 → 页面自动更新。
选项式:在 actions 里定义 increment / decrement
为什么需要:选项式最接近原来的 Vuex 习惯,适合先建立对照。
怎么想:state 放数据,actions 放方法;方法里通过 this 拿到当前 store 的数据。
怎么做:在 store 对象中写 actions,定义加、减两个方法:
js
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
})页面中点击时调用:
vue
<button @click="store.increment()">加</button>
<button @click="store.decrement()">减</button>转写稿中通过调试确认:点击加号,count 增加;点击减号,count 减少,页面自动更新,说明数据是响应式的。
易错点:方法名 increment、decrement 容易拼错;actions 里用 this.count 时,要保证这里的 this 指向当前 store;模板调用方法时不要漏掉括号。
组合式:普通函数 + ref.value + return
为什么需要:组合式是转写稿明确推荐的写法,也是 Pinia 更常用的 setup 方式。
怎么想:Vue 3 组合式 API 中,事件回调本来就是普通函数,所以不需要套 actions,直接写 function 即可。但修改 ref 数据时必须用 .value。
怎么做:
js
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value += 1
}
function decrement() {
count.value -= 1
}
return { count, increment, decrement }
})页面调用方式不变:
vue
<button @click="store.increment()">加</button>
<button @click="store.decrement()">减</button>转写稿里有一个关键调试过程:一开始只写了 increment、decrement 函数,但忘了 return,点击时页面报错,提示它们“不是函数”。原因是组合式 store 里定义的数据和方法,必须 return 出去,页面才能从 store 上拿到。补上 return { count, increment, decrement } 后,再加、减就正常了。
易错点:第一,改 ref 数据必须写 count.value;第二,数据和函数都要 return;第三,不要把 count 写成 count.value 后又忘记 return。
和 Vuex 对照记忆,并用调试工具检查
转写稿最后给了三条对照原则:ref 定义的数据相当于 Vuex 的 state;computed 相当于 getters;要定义函数时,组合式里直接写普通函数即可。选项式则用 actions 放方法。
调试时打开 Vue 调试插件,在 Pinia 面板中可以看到 store 名称,例如 counter,还能看到 count、name 等数据。转写稿特别提醒:store 的名字会显示在调试插件里,不要随便乱叫,否则数据多时自己都分不清。
易错点:不要只记住“能跑”,还要检查 store 是否真的暴露了方法;方法名、store 名、ref 的 .value、return 这四处是本节最容易出错的地方。
先建立对应关系:Vuex 的思路怎么迁到 Pinia
为什么需要:如果只死记 Pinia 语法,很容易和 Vuex 混在一起;转写稿里反复用 Vuex 做类比,就是为了让迁移有抓手。怎么想:把 state 想成数据,把 getters 想成计算属性,把 actions 想成方法。怎么做:选项式写法里,state 返回对象,getters 写派生数据,actions 写方法;组合式写法里,数据用 ref,计算属性用 computed,方法就是普通函数。易错点:组合式里定义的数据和函数都要 return 出去;改 ref 数据要写 .value;调试插件里显示的 store 名字就是 defineStore('counter', ...) 里的 counter,不要乱起名。
js
// 选项式
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: '小明' }),
getters: {
double() { return this.count * 2 }
},
actions: {
increment() { this.count++ },
decrement() { this.count-- }
}
})js
// 组合式
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('小明')
const double = computed(() => count.value * 2)
function increment() { count.value += 1 }
function decrement() { count.value -= 1 }
return { count, name, double, increment, decrement }
})计算属性:选项式 getters 与组合式 computed
为什么需要:页面上的“二倍值”不应再单独存一份数据,否则要手动同步,容易漏改。怎么想:double 是由 count 推导出来的,所以用计算属性。转写稿先写选项式 getters,页面显示 10 对应 20;再换成组合式 computed,点击加减仍然保持二倍关系,说明功能一致。怎么做:选项式在 getters 里 return this.count * 2;组合式写 const double = computed(() => count.value * 2),最后 return double。易错点:组合式 computed 里访问 ref 必须 .value;模板里用 store.double,不要写成 store.double();忘记 return double 时页面拿不到。
js
const double = computed(() => count.value * 2)
return { count, name, double, increment, decrement }登录 Tab 切换:用计算属性取当前项
为什么需要:密码登录和验证码登录的标题、副标题要随点击切换,不能把两套文字写死。怎么想:定义数组 tabMetas,第 0 项是一组文案,第 1 项是一组文案;再用 tabIndex 记录当前索引,用计算属性 tabMeta 根据索引取当前项。转写稿的推导是:1 - 1 = 0,0 - 1 = -1,取绝对值又变 1,所以 Math.abs(tabIndex.value - 1) 可以在 0 和 1 之间切换。怎么做:
js
const tabMetas = [
{ title: '密码登录', subTitle: '验证码登录' },
{ title: '验证码登录', subTitle: '密码登录' }
]
const tabIndex = ref(0)
const tabMeta = computed(() => tabMetas[tabIndex.value])
function onSubTitleClick() {
tabIndex.value = Math.abs(tabIndex.value - 1)
}页面用 {{ tabMeta.title }}、{{ tabMeta.subTitle }}。易错点:tabMetas 是普通数组,不用 .value;tabIndex 是 ref,计算属性里和事件里都要 .value;模板里 tabMeta.title 会自动解包,不要再写 .value。调试时在 Vue Devtools 切到组件面板,看 tabIndex 是否在 0/1 间变化,再看 tabMeta 是否取到对应项。
方法定义与响应式修改:actions 与普通函数
为什么需要:点加号、减号要改 count,必须把事件和修改逻辑接上。怎么想:选项式把方法放 actions,里面用 this.count++;组合式直接写普通函数,改 count.value。怎么做:模板里 @click="store.increment()",组合式函数里 count.value += 1。易错点:组合式函数没 return 时,控制台会报 increment is not a function,转写稿就是漏了 return,补上后刷新恢复正常;改 ref 必须 .value;调用 store 方法要加括号。
登录页局部组件与条件渲染
为什么需要:密码表单和验证码表单要拆成组件,页面只负责切换。怎么想:只跟当前页面相关的组件放在页面下的 components 目录,手动 import,不必用 easycom 全局自动引入。怎么做:import CustomPassword from './components/password.vue',默认导出不加花括号;注册到 components;模板用 v-if="tabIndex === 0" 显示密码组件,否则显示验证码组件。易错点:默认导入不要加大括号;组件名注册后模板要用注册名;v-if 判断的是 tabIndex;组件样式隔离和穿透点要复习,避免公共样式互相污染。
代码示例
1. Pinia 两种写法:选项式(options)与组合式(setup)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Pinia 选项式 vs 组合式</title>
</head>
<body>
<div id="app">
<p>count:{{ store.count }}</p>
<p>name:{{ store.name }}</p>
<p>double(getters):{{ store.double }}</p>
<button @click="store.increment">+</button>
<button @click="store.decrement">-</button>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/pinia@2/dist/pinia.iife.js"></script>
<script>
const { createApp, ref, computed } = Vue
const { createPinia, defineStore } = Pinia
// ① 选项式写法:和 Vuex 几乎一样
// state -> 数据,getters -> 计算,actions -> 方法(里面用 this 取数据)
const useOptionStore = defineStore('counterOption', {
state: () => ({
count: 0,
name: '小明'
}),
getters: {
double: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
}
}
})
// ② 组合式(setup)写法:推荐
// ref -> state,computed -> getters,普通 function -> actions
const useSetupStore = defineStore('counterSetup', () => {
const count = ref(0)
const name = ref('小明')
const double = computed(() => count.value * 2)
function increment() {
count.value += 1 // ref 在任何地方改值都要 .value
}
function decrement() {
count.value -= 1
}
// 所有要暴露给组件用的东西,必须 return
return { count, name, double, increment, decrement }
})
const App = {
setup() {
// 换成 useOptionStore() 效果完全一样
const store = useSetupStore()
return { store }
}
}
createApp(App).use(createPinia()).mount('#app')
</script>
</body>
</html>关键行与易错点
defineStore('counterSetup', () => {...}):第二个参数传函数就是组合式(setup)写法;传对象就是选项式(options)写法,两者功能一致,官方推荐组合式。- 对照关系要背下来:
state → ref、getters → computed、actions → 普通 function。 - 组合式里必须 return。少 return 一个函数,页面点击时会报「increment 不是一个函数」;少 return 一个 ref,模板里就取不到数据。
- 组合式里改值一定要
count.value += 1,写成count += 1会变成字符串拼接或直接报错;但在模板和 store 实例上访问时不需要.value(store.count会自动解包)。 defineStore的第一个参数是 store 的 id,会显示在 Vue Devtools 的 Pinia 面板里,名字别乱起,否则调试时认不出是哪个 store。
2. 登录 tab 切换:ref 存索引 + computed 取数据 + 绝对值切换
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>登录 tab 切换</title>
<style>
.tab { display: flex; justify-content: space-between; padding: 12px; background: #fff; }
.sub { color: #1989fa; cursor: pointer; }
</style>
</head>
<body>
<div id="app">
<div class="tab">
<span>{{ tabMeta.title }}</span>
<span class="sub" @click="onSubTitleClick">{{ tabMeta.subTitle }}</span>
</div>
<p>当前索引:{{ typeIndex }}</p>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref, computed } = Vue
const App = {
setup() {
// 人为构造的「一对一对」的数据,靠索引切换
const tabList = [
{ title: '密码登录', subTitle: '验证码登录' },
{ title: '验证码登录', subTitle: '密码登录' }
]
// 记录索引值,初始 0 表示显示第一条
const typeIndex = ref(0)
// 计算属性:索引一变,取出来的对象跟着变
const tabMeta = computed(() => tabList[typeIndex.value])
// 0 和 1 之间的切换:0-1 = -1 -> abs -> 1;1-1 = 0 -> abs -> 0
function onSubTitleClick() {
typeIndex.value = Math.abs(typeIndex.value - 1)
}
return { typeIndex, tabMeta, onSubTitleClick }
}
}
createApp(App).mount('#app')
</script>
</body>
</html>关键行与易错点
tabMeta = computed(() => tabList[typeIndex.value]):computed 里必须读.value,并且必须有返回值(写成{ tabList[typeIndex.value] }就变成返回对象了)。- 模板里写
{{ tabMeta.title }},不要写tabMeta.value.title——模板中 ref/computed 会自动解包。 - 切换核心是
Math.abs(typeIndex.value - 1):0 → 1、1 → 0。漏掉Math.abs会出现-1,此时tabList[-1]是undefined,页面直接白掉/报错。 - 算完一定要重新赋值
typeIndex.value = ...,只写Math.abs(...)不赋值等于没改。 - 只有两种状态时别上
v-for循环,一个computed+ 索引就够,逻辑更清晰。
3. 登录页:tab 切换 + 两个表单组件按条件显示
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>登录页 - 表单组件条件渲染</title>
<style>
.tab { display: flex; justify-content: space-between; padding: 12px; background: #fff; }
.sub { color: #1989fa; cursor: pointer; }
.form { padding: 16px; border: 1px solid #eee; margin-top: 8px; }
</style>
</head>
<body>
<div id="app">
<div class="tab">
<span>{{ tabMeta.title }}</span>
<span class="sub" @click="onSubTitleClick">{{ tabMeta.subTitle }}</span>
</div>
<!-- 等于 0 显示密码登录表单,等于 1 显示验证码登录表单 -->
<custom-password v-if="typeIndex === 0"></custom-password>
<custom-mobile v-if="typeIndex === 1"></custom-mobile>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref, computed } = Vue
// 只在本页面使用的组件:局部注册即可(真实项目里就是
// import CustomPassword from './components/password.vue',不加花括号)
const CustomPassword = {
name: 'CustomPassword',
template: `<div class="form">密码登录表单:账号 + 密码</div>`
}
const CustomMobile = {
name: 'CustomMobile',
template: `<div class="form">验证码登录表单:手机号 + 获取验证码</div>`
}
const App = {
components: { CustomPassword, CustomMobile },
setup() {
const tabList = [
{ title: '密码登录', subTitle: '验证码登录' },
{ title: '验证码登录', subTitle: '密码登录' }
]
const typeIndex = ref(0)
const tabMeta = computed(() => tabList[typeIndex.value])
function onSubTitleClick() {
typeIndex.value = Math.abs(typeIndex.value - 1)
}
return { typeIndex, tabMeta, onSubTitleClick }
}
}
createApp(App).mount('#app')
</script>
</body>
</html>关键行与易错点
v-if="typeIndex === 0"/v-if="typeIndex === 1":两个表单只能显示一个,别用v-show之外的取反条件写反了;模板里typeIndex直接当值用,不要写typeIndex.value。- 局部注册组件时
import CustomPassword from './components/password.vue'不加花括号(默认导出),组件名建议统一用Custom前缀驼峰,避免和全局组件撞名;只在某个页面用的组件就在页面同级建components目录,别丢到全局。 - 在 in-DOM 模板(直接写在 HTML 里)中,
<custom-password />这种自闭合写法不生效,浏览器会把它当成未闭合标签,把后面的custom-mobile吞进去,所以必须写</custom-password>;写成 SFC 的.vue文件才允许自闭合。 - 组件内部
<style scoped>的作用是样式隔离,同时配合:deep()才能改 UI 组件库(如 uni-form)内部样式——这是上一节复习过的点。
常见错误
现象:组合式 store 在页面里调用
increment报“不是函数”或undefined。
原因:setup里定义了方法,但忘记return暴露。
修法:把count、increment、decrement、double等统一放进return { ... }。现象:组合式方法里执行了
count++,页面数据不变或直接报错。
原因:count是ref,在 JS 逻辑中未通过.value修改。
修法:写成count.value++/count.value--;模板中使用时不需要.value。现象:选项式
actions里this.count拿不到,报undefined。
原因:action 写成箭头函数,this不是 store 实例。
修法:action 用普通函数定义,如increment() { this.count++ }。现象:计算属性里
tabs[tabIndex]取不到当前项。
原因:tabIndex是ref,在 JS 中访问索引时漏了.value。
修法:计算属性内部写tabs[tabIndex.value];页面模板里用typeMeta.title,不要机械加.value。现象:导入
password、mobile组件后页面不渲染,或使用时报未定义。
原因:默认导出组件被加了大括号,或导入名与模板使用名不一致。
修法:页面级组件直接默认导入,如import CustomPassword from './components/password.vue',模板使用对应注册名。现象:点击 tab 后索引变成
-1,或两个登录表单同时显示。
原因:0/1 切换时直接减一未处理负数;v-if条件未互斥。
修法:用Math.abs(tabIndex.value - 1)或1 - tabIndex.value;两个表单用=== 0与=== 1,或v-if / v-else保证只显示一个。现象:Devtools 里找不到 store 或分不清数据来源。
原因:store id 命名随意,调试时未刷新插件。
修法:给 store 固定、清晰的 id;先刷新页面,再在 Vue Devtools 的 Pinia/组件面板核对 state、getter 和组件内 ref。
练习
用选项式和组合式各写一个 counter store:
count初始值为 10,提供increment、decrement,并提供二倍值double。要求页面点击加减后count与double同步变化。在登录页 tab 中新增第三项“扫码登录”:修改 tabs 数据、切换逻辑和表单显示条件,保证任意时刻只显示一个登录表单。
给下面错误片段找错并修正:组合式 store 中写了
const count = ref(0)、function add() { count++ }、return { count },页面调用add后数据不变。把
password与mobile两个表单组件接入登录页:默认显示密码登录,点击切换后显示验证码登录,并用 Devtools 检查tabIndex、typeMeta是否正确。用 Devtools 验证一次完整链路:store 中的
count、double、方法调用前后变化,以及组件中的tabIndex与当前 tab 数据。记录一次因命名不清或漏return导致的排查过程。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 优医咨询-Pinia选项式和组合式 | 09:42 |
| 2 | 优医咨询-定义方法 | 08:09 |
| 3 | 优医咨询-计算属性 | 05:42 |
| 4 | 优医咨询-登录页面布局 | 08:04 |
| 5 | 优医咨询-登录tab切换-1 | 08:03 |
| 6 | 优医咨询-登录tab切换-2 | 06:52 |
| 7 | 优医咨询-表单组件 | 10:57 |
小结
- 选项式写法适合对照 Vuex 理解迁移,组合式写法是当前更推荐的落地方案。
- 组合式 store 的关键不是“写完”,而是“暴露完”:数据、方法、计算属性都要能通过
return被页面拿到。 ref的.value只出现在 JS 逻辑里;模板自动解包,选项式则用this访问。- 页面级组件就近放在页面目录下,直接默认导入;不要和全局组件、Easycom 的使用方式混在一起。
- tab 切换用“索引驱动”最稳:数组提供文案,计算属性取当前项,事件改索引,
v-if控制表单互斥显示。 - 0/1 切换优先用
1 - x或Math.abs(x - 1),比临时分支更不容易把索引写成负数。 - 调试顺序建议固定:先看 Devtools 的 store id/state/getter,再看组件内 ref 与 computed,最后查模板条件。
- 先搭页面结构和样式,再接 store 与交互;每完成一小段就保存、刷新、验证,避免错误堆叠。