Skip to content

组件通信与表单组件(四) ​

本页对应 组件通信与表单组件,共 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)
}

页面用 &#123;&#123; tabMeta.title &#125;&#125;、&#123;&#123; tabMeta.subTitle &#125;&#125;。易错点: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] } 就变成返回对象了)。
  • 模板里写 &#123;&#123; tabMeta.title &#125;&#125;,不要写 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)内部样式——这是上一节复习过的点。

常见错误 ​

  1. 现象:组合式 store 在页面里调用 increment 报“不是函数”或 undefined。
    原因:setup 里定义了方法,但忘记 return 暴露。
    修法:把 count、increment、decrement、double 等统一放进 return { ... }。

  2. 现象:组合式方法里执行了 count++,页面数据不变或直接报错。
    原因:count 是 ref,在 JS 逻辑中未通过 .value 修改。
    修法:写成 count.value++ / count.value--;模板中使用时不需要 .value。

  3. 现象:选项式 actions 里 this.count 拿不到,报 undefined。
    原因:action 写成箭头函数,this 不是 store 实例。
    修法:action 用普通函数定义,如 increment() { this.count++ }。

  4. 现象:计算属性里 tabs[tabIndex] 取不到当前项。
    原因:tabIndex 是 ref,在 JS 中访问索引时漏了 .value。
    修法:计算属性内部写 tabs[tabIndex.value];页面模板里用 typeMeta.title,不要机械加 .value。

  5. 现象:导入 password、mobile 组件后页面不渲染,或使用时报未定义。
    原因:默认导出组件被加了大括号,或导入名与模板使用名不一致。
    修法:页面级组件直接默认导入,如 import CustomPassword from './components/password.vue',模板使用对应注册名。

  6. 现象:点击 tab 后索引变成 -1,或两个登录表单同时显示。
    原因:0/1 切换时直接减一未处理负数;v-if 条件未互斥。
    修法:用 Math.abs(tabIndex.value - 1) 或 1 - tabIndex.value;两个表单用 === 0 与 === 1,或 v-if / v-else 保证只显示一个。

  7. 现象:Devtools 里找不到 store 或分不清数据来源。
    原因:store id 命名随意,调试时未刷新插件。
    修法:给 store 固定、清晰的 id;先刷新页面,再在 Vue Devtools 的 Pinia/组件面板核对 state、getter 和组件内 ref。

练习 ​

  1. 用选项式和组合式各写一个 counter store:count 初始值为 10,提供 increment、decrement,并提供二倍值 double。要求页面点击加减后 count 与 double 同步变化。

  2. 在登录页 tab 中新增第三项“扫码登录”:修改 tabs 数据、切换逻辑和表单显示条件,保证任意时刻只显示一个登录表单。

  3. 给下面错误片段找错并修正:组合式 store 中写了 const count = ref(0)、function add() { count++ }、return { count },页面调用 add 后数据不变。

  4. 把 password 与 mobile 两个表单组件接入登录页:默认显示密码登录,点击切换后显示验证码登录,并用 Devtools 检查 tabIndex、typeMeta 是否正确。

  5. 用 Devtools 验证一次完整链路:store 中的 count、double、方法调用前后变化,以及组件中的 tabIndex 与当前 tab 数据。记录一次因命名不清或漏 return 导致的排查过程。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1优医咨询-Pinia选项式和组合式09:42
2优医咨询-定义方法08:09
3优医咨询-计算属性05:42
4优医咨询-登录页面布局08:04
5优医咨询-登录tab切换-108:03
6优医咨询-登录tab切换-206:52
7优医咨询-表单组件10:57

小结 ​

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