Skip to content

面经 PC 端(上)(一) ​

本页对应 面经 PC 端(上),共 7 段视频,约 1 小时 34 分。

学习目标 ​

  • 能说明 Vuex 解决大范围组件数据共享的机制,并区分 state、getters、mutations、actions 的职责与调用关系。
  • 能完成 Vuex 3.6.2 的安装与配置:创建 store/index.js、在 main.js 中挂载,并通过 Vue 调试工具确认 Vuex 生效。
  • 能在 state 中定义共享数据、在 getters 中定义派生数据,并在组件中通过 $store 或 mapState、mapGetters 正确读取,能处理命名冲突。
  • 能在 mutations 中编写同步更新 state 的方法并通过 commit 调用传参,能在 actions 中编写异步方法并通过 dispatch 调用,在 action 内 commit mutation 更新数据。
  • 能完成一级路由的配置与路由出口渲染,并通过路径切换验证页面跳转生效。
  • 能配置 tabbar 标签页并设置主题色,验证标签选中态与主题样式正确生效。

知识点 ​

一级路由的设计思路 ​

为什么需要:做后台/PC端项目时,页面不是全都平铺的。登录、注册、文章详情是独立展示的页面;首页则是一个带导航和内容区的“壳”,里面再切换面经、收藏、喜欢、我的。如果不先分清一级路由和二级路由,后面写嵌套路由时路径会乱,组件也不知道往哪里放。

怎么想:判断标准很直接——但凡是单个页面、独立展示的,都是一级路由。首页 Layout 本身也是一级路由,它负责承载二级路由。按笔记中的设计:

  • 登录页(一级)Login
  • 注册页(一级)Register
  • 文章详情页(一级)Detail
  • 首页(一级)Layout
    • 面经(二级)Article
    • 收藏(二级)Collect
    • 喜欢(二级)Like
    • 我的(二级)My

一级路由与二级路由设计图

怎么做:先只配置一级路由,二级路由等 Layout 结构准备好后再配。这样调试时能先确认每个独立页面是否能通过地址访问。

易错点:不要把面经、收藏等直接配成一级路由;它们应作为 Layout 的子路由。文章详情页虽然也独立展示,但需要携带文章 id,所以路径要设计成动态参数形式。

配置 router/index.js ​

为什么需要:Vue Router 需要把“路径”和“组件”建立映射。一级路由配置好后,访问 /login 就渲染 Login,访问 /register 就渲染 Register。

怎么想:转写稿里回顾路由守卫时提到,导航守卫要写在“创建 router 变量之后、导出之前”,因为守卫里要用到 router 变量。这个位置意识同样适用于路由配置:先导入组件,再 Vue.use(VueRouter),再 new VueRouter({ routes }),最后导出。

怎么做:在 router/index.js 中配置一级路由,一级 views 组件从准备好的目录中直接复制过来即可:

jsx
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    { path: '/login', component: Login },
    { path: '/register', component: Register },
    { path: '/article/:id', component: Detail },
    {
      path: '/',
      component: Layout
    }
  ]
})
export default router

易错点:

  • Vue.use(VueRouter) 必须在创建 router 实例前执行。
  • 组件导入路径要写对,笔记中使用 @/views/...。
  • 文章详情页路径是 /article/:id,id 是动态参数,不能漏掉冒号。
  • 根路径 / 指向 Layout,是为了让首页承载二级路由;不要随意改成别的路径,否则后续子路由基准会变。
  • 最后一定要 export default router,并在入口中挂载。

清理 App.vue ​

为什么需要:一级路由配置好后,页面内容应该由路由出口决定。如果 App.vue 里还保留默认内容或重复结构,容易出现“路由页面出来了,但外面还套着旧内容”的情况。

怎么想:App.vue 只保留最外层容器和 <router-view/>,让匹配到的一级路由组件在这里渲染。转写稿中反复强调路由相关配置要围绕 router 展开,App.vue 不需要承担页面结构。

怎么做:把 App.vue 清理成:

jsx
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  created () {

  }
}
</script>

易错点:<router-view/> 不要拼错;如果页面空白,先检查入口是否挂载了 router、router/index.js 是否导出、App.vue 是否保留路由出口。调试时可以先逐个访问 /login、/register、/article/1、/,确认一级路由都能独立渲染,再继续配置 Layout 下的二级路由。

本节目标与效果 ​

本节要完成的是移动端首页架子底部的 Tabbar 标签栏:在布局组件 layout.vue 中放置四个底部标签——面经、收藏、喜欢、我的,并使用 Vant 的 Tabbar 组件完成静态展示。最终效果参考下图:

底部导航栏效果图

组件文档地址:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/tabbar

为什么需要:移动端项目通常需要底部固定导航,让用户在几个一级页面之间快速切换。把 Tabbar 放在 layout.vue 这类“首页架子”里,可以让多个页面共用同一套底部导航,避免每个页面重复写一遍。
怎么想:先不要自己从零写结构和样式,优先复制 Vant 官方示例,保证组件结构正确,再改文本和图标。转写稿里讲师反复强调配置类代码要“按固定位置、固定语法写,先按文档来”,Tabbar 也遵循这个思路。
怎么做:先注册组件,再在布局组件中复制官方 Tabbar 代码,最后替换成项目需要的四个标签。
易错点:Vue2 项目要对应 Vant2 文档,不要混用新版语法;组件必须先注册;图标名要使用 Vant 图标库中的名称,不能随便写图片路径。

组件引入与注册 ​

为什么需要:Vant 的组件不是全局自动可用的,尤其在 Vue2 项目中,需要通过 Vue.use() 注册后才能直接在模板里写 <van-tabbar>、<van-tabbar-item>。
怎么想:项目里已经有统一的 vant-ui.js 入口,就继续在这个文件里按需引入和注册,保持组件管理集中。
怎么做:在 vant-ui.js 中引入 Tabbar、TabbarItem,并调用 Vue.use():

jsx
import { Button, Icon, Tabbar, TabbarItem } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)

这里保留原来的 Button、Icon,新增的是 Tabbar 和 TabbarItem。
易错点:只引入不注册,页面会提示组件未注册;Tabbar 和 TabbarItem 是两个组件,不能只注册外层而漏掉子项;不要从错误版本的 Vant 包中引入。

在 layout.vue 中落地 ​

为什么需要:layout.vue 是“首页架子”,负责内容区域和公共底部导航。Tabbar 放在这里,后续页面切换时底部栏可以保持稳定。
怎么想:先复制官方代码,保证 <van-tabbar> 内部有多个 <van-tabbar-item>,再按项目需求改文字和图标。
怎么做:在 layout.vue 中写入以下结构:

jsx
<template>
  <div class="layout-page">
    首页架子 - 内容区域 
    <van-tabbar>
      <van-tabbar-item icon="notes-o">面经</van-tabbar-item>
      <van-tabbar-item icon="star-o">收藏</van-tabbar-item>
      <van-tabbar-item icon="like-o">喜欢</van-tabbar-item>
      <van-tabbar-item icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>

易错点:不要漏掉 van-tabbar-item 的闭合标签;文本要写在标签内部,图标通过 icon 属性指定;本节只完成静态标签栏,路由跳转不要提前硬编,按后续路由配置继续完成。

修改文本与图标 ​

为什么需要:官方示例只是通用结构,项目需要的是“面经、收藏、喜欢、我的”四个业务入口,所以必须替换文本和图标。
怎么想:文本直接改标签内容;图标从 Vant 文档的图标列表中选语义接近的:面经用 notes-o,收藏用 star-o,喜欢用 like-o,我的用 user-o。
怎么做:按上面代码逐个替换。完成后启动项目,检查底部是否出现四个标签,文字是否对应,图标是否显示。
易错点:图标名写错时通常不显示图标;不要把 icon 写成 src;如果四个标签挤在一起或样式异常,先检查是否使用了 Vant2 的 Tabbar 结构,而不是自己改坏了 van-tabbar-item。

调试与检查 ​

为什么需要:组件配置类问题往往不是逻辑错误,而是注册、版本、名称、结构其中之一出错。
怎么想:按“组件是否注册 → 模板结构是否完整 → 图标名是否正确 → 页面是否在 layout 中渲染”的顺序排查。
怎么做:先确认 vant-ui.js 已执行,再确认 layout.vue 中标签结构完整,最后看浏览器页面底部效果。
易错点:不要因为图标不显示就重写整个组件;先查 Vue.use(Tabbar)、Vue.use(TabbarItem) 是否遗漏。完成本节后,Tabbar 只是静态展示,后续再结合路由配置实现点击切换。

为什么需要配置主题色 ​

为什么需要:笔记开头指出,整体网站风格其实都是橙色的。如果继续使用 Vant 默认主题色,页面中的按钮、导航、选中态等主色会与项目整体风格不一致。逐个组件写样式覆盖,既零散又容易漏。

怎么想:不要直接改 node_modules 里的 Vant 源码,也不要在每个页面里重复覆盖颜色。Vant 支持通过变量覆盖的方式制定主题色,所以思路是:让 Vant 的按需引入走 Less 源样式,再在 Vue CLI 的 Less 配置里用 modifyVars 覆盖主题变量。参考文档:
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/theme

怎么做:分两步,先改 babel.config.js 指定样式路径,再改 vue.config.js 覆盖变量。配置完成后重启服务器生效。

易错点:不要直接改 node_modules/vant;不要只在某个组件里改颜色;变量覆盖要写在 Vue CLI 的 Less loader 配置中,而不是随便写在业务样式里。

在 babel.config.js 中指定样式路径 ​

为什么需要:Vant 按需引入时,默认可能引入编译后的样式。要让 Less 变量覆盖真正参与编译,需要让按需引入插件去加载 Vant 的 Less 源样式,所以笔记在 babel.config.js 中指定样式路径。

怎么想:babel-plugin-import 的 style 选项可以控制引入哪种样式。这里把样式路径写成 ${name}/style/less,意思是让 Vant 组件按需引入时走 Less 样式入口。

怎么做:保留笔记中的配置:

jsx
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      // 指定样式路径
      style: (name) => `${name}/style/less`
    }, 'vant']
  ]
}

易错点:libraryName 要写 'vant';libraryDirectory 是 'es';style 是函数,返回的是模板字符串 ${name}/style/less,不要漏掉反引号或写成普通字符串;插件数组最后的 'vant' 也不要漏。改完 Babel 配置后不能只刷新页面,要重启开发服务器。

在 vue.config.js 中覆盖 Less 变量 ​

为什么需要:上一步只是让样式走 Less,真正把默认主题色换成项目橙色,需要在 Less 编译时覆盖 Vant 的主题变量。笔记中通过 modifyVars 直接覆盖变量。

怎么想:Vue CLI 项目可以在 vue.config.js 的 css.loaderOptions.less 中配置 Less loader。lessOptions.modifyVars 用来修改变量值。笔记示例把 'blue' 覆盖为 '#FA6D1D',也就是把对应的主色变量改成橙色。

怎么做:保留笔记中的配置:

jsx
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  css: {
    loaderOptions: {
      less: {
        lessOptions: {
          modifyVars: {
            // 直接覆盖变量
            'blue': '#FA6D1D',
          },
        },
      },
    },
  },
})

易错点:层级不能少,必须是 css.loaderOptions.less.lessOptions.modifyVars;变量名要和 Vant 主题文档中的变量名一致,笔记这里覆盖的是 'blue';颜色值要带 #;不要写成 css.modifyVars 或 loaderOptions.modifyVars;如果项目里已有 vue.config.js,要在原有配置上合并,不要粗暴覆盖掉其他配置。

重启服务器并验证是否生效 ​

为什么需要:babel.config.js 和 vue.config.js 都属于构建配置,修改后开发服务器不会像普通业务代码那样可靠地热更新。笔记明确写了“重启服务器生效”。

怎么想:先停止当前开发服务器,再重新启动;然后打开页面看 Vant 的主色相关元素是否变成橙色。笔记没有给出本节截图,验证时以页面主色是否变为 #FA6D1D 为准。

怎么做:重启项目后检查按钮、导航、选中态等使用主题色的位置。如果仍是默认色,按顺序排查:babel.config.js 是否走了 style/less;vue.config.js 的 Less 配置层级是否正确;变量名是否与 Vant 文档一致;是否真的重启了服务器。

易错点:只刷新浏览器不算重启;只改 vue.config.js 而忘了 babel.config.js 的 Less 样式路径,变量覆盖可能不生效;改完配置后看到旧颜色,先确认终端里的开发服务器已经重新启动,而不是继续怀疑变量值写错。

课件图示 ​

课件第16页:目标:阅读vant组件库文档,实现底部导航 tabbar 路由设计配置 tabb

课件第15页:目标:分析项目页面,设计路由,配置一级路由 路由设计配置 登录 注册 首页 面经

课件第18页:目标:基于底部导航,完成二级路由配置 路由设计配置 配置二级路由 配置导航链接

课件第17页:目标:通过 配置主题色,实现底部导航定制 路由设计配置 配置主题色: ① bab

课件第3页:项目收获 目标:明确做完本项目,能够收获哪些内容 脚手架自定义项目构建 组件库v

课件第8页:调整初始化目录 目标:将目录调整成符合企业规范的目录 1. 删除 多余的文件 2

课件第45页:目标:配置路由懒加载,实现打包优化 打包优化:路由懒加载 说明:当打包构建应用时

课件第4页:创建项目 目标:基于 VueCli 自定义创建项目架子 安装脚手架 (√) 创建

代码示例 ​

代码一:一级路由配置 + 清理 App.vue ​

js
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: [
    { path: '/login', component: Login },
    { path: '/register', component: Register },
    { path: '/article/:id', component: Detail },
    {
      path: '/',
      component: Layout
    }
  ]
})

export default router
vue
<!-- src/App.vue -->
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  created () {

  }
}
</script>

关键行与易错点

  • Vue.use(VueRouter) 必须在 new VueRouter 之前调用,否则 $route/$router 全都不存在。
  • 一级路由的判定标准就是“单个页面、独立展示”:登录、注册、详情、Layout 都是平级的四个一级路由;面经/收藏/喜欢/我的属于 Layout 内部的二级路由。
  • { path: '/', component: Layout } 这里写的是 /,不是 /layout,首页地址才会是根路径。
  • '/article/:id' 是动态路由参数,详情页取参数用 this.$route.params.id($route 是取参数,$router 是跳转,别混)。
  • @ 是 src 别名,路径写成 @/views/Login 而不是相对路径,避免层级变化后 import 全部失效。
  • App.vue 必须清掉脚手架自带的 logo/HelloWorld,只保留 <router-view/>,否则每个页面顶部都会多出一块内容。

代码二:Layout 里挂 tabbar + 覆盖 vant 主题色 ​

js
// src/utils/vant-ui.js
import Vue from 'vue'
import { Button, Icon, Tabbar, TabbarItem } from 'vant'

Vue.use(Button)
Vue.use(Icon)
Vue.use(Tabbar)
Vue.use(TabbarItem)
vue
<!-- src/views/Layout/index.vue -->
<template>
  <div class="layout-page">
    首页架子 - 内容区域
    <van-tabbar>
      <van-tabbar-item icon="notes-o">面经</van-tabbar-item>
      <van-tabbar-item icon="star-o">收藏</van-tabbar-item>
      <van-tabbar-item icon="like-o">喜欢</van-tabbar-item>
      <van-tabbar-item icon="user-o">我的</van-tabbar-item>
    </van-tabbar>
  </div>
</template>
js
// babel.config.js
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ],
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      // 指定样式路径
      style: (name) => `${name}/style/less`
    }, 'vant']
  ]
}
js
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  css: {
    loaderOptions: {
      less: {
        lessOptions: {
          modifyVars: {
            // 直接覆盖变量
            'blue': '#FA6D1D'
          }
        }
      }
    }
  }
})

关键行与易错点

  • tabbar 的组件必须在 vant-ui.js 里 Vue.use(Tabbar)、Vue.use(TabbarItem) 注册,只把模板贴过来会报 Unknown custom element: <van-tabbar>。
  • icon="notes-o" 这些图标名来自 vant 图标库,改文本时要连图标一起改,别只改文字。
  • style: (name) => \${name}/style/less`是主题定制的开关:样式路径必须指向 less,后面modifyVars` 才有东西可覆盖。
  • modifyVars 里的变量名按 vant 官方主题文档的变量写,覆盖后整站按钮、tabbar 选中色一起变。
  • 改完 babel.config.js 或 vue.config.js 必须重启 dev server,热更新不会生效,这是最容易卡住的一步。

代码三:Vuex 仓库(state / getters / mutations / actions)+ 组件使用 ​

js
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  strict: true,
  state: {
    age: 20,
    uname: '张三',
    list: [
      { id: 1, name: '吃饭', isDone: true },
      { id: 2, name: '睡觉', isDone: false },
      { id: 3, name: '听歌', isDone: true }
    ]
  },
  getters: {
    // 第一个参数固定是 state
    abc: (state) => state.age * state.age,
    isAll: (state) => state.list.every(item => item.isDone === true)
  },
  mutations: {
    // 第一个参数固定是 state
    updateAge (state, n) {
      state.age = n
    }
  },
  actions: {
    // 第一个参数固定是 store,不是 state
    updateAgeAfterSomeTime (store, n) {
      setTimeout(() => {
        store.commit('updateAge', n)
      }, 3000)
    }
  }
})

export default store
js
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
vue
<!-- src/views/Test/index.vue -->
<template>
  <div>
    <p>直接使用:{{ $store.state.age }} / {{ $store.getters.abc }}</p>
    <p>简化使用:{{ age }} {{ uname }} {{ abc }} {{ isAll }}</p>

    <button @click="$store.commit('updateAge', 1000)">更新 age</button>
    <button @click="$store.dispatch('updateAgeAfterSomeTime', 500)">3 秒后更新</button>
  </div>
</template>

<script>
import { mapState, mapGetters } from 'vuex'

export default {
  computed: {
    // 数组写法:名字和 state 里的 key 一致
    ...mapState(['age', 'uname', 'list']),
    ...mapGetters(['abc', 'isAll'])

    // 有重名冲突时改成对象写法改名,例如:
    // ...mapState({ nianling: 'age' })
  }
}
</script>

关键行与易错点

  • 四个选项是并列关系:state 存数据、getters 相当于 store 的计算属性(只读,不支持 set)、mutations 是同步修改 state 的唯一途径、actions 只放异步任务。
  • mutations 的第一个参数固定是 state;actions 的第一个参数固定是 store——因为 action 里不能直接改 state,只能 store.commit('updateAge', n) 去调 mutation 改,这一点是最容易写错的。
  • 在组件里直接 $store.state.age++ 虽然值会变,但 devtools 抓不到变化;开了 strict: true 还会直接报错,所以一律走 commit。
  • 组件调用语法是固定的:改数据用 this.$store.commit('方法名', 参数),异步用 this.$store.dispatch('方法名', 参数)。
  • mapState / mapGetters 必须展开写在 computed 里,mapMutations / mapActions 写在 methods 里;写到 data 里是无效的。
  • 用辅助方法后,如果和组件自身 data/computed 重名(比如组件自己也有 age),要写成对象格式改名:...mapState({ nianling: 'age' }),否则报“已定义过”。

常见错误 ​

  1. 导航守卫不生效
    现象:未登录仍能进入页面,或守卫代码写了但无反应。
    原因:守卫写在 export default router 之后,或未放在创建 router 实例之后。
    修法:在 new VueRouter() 之后、export default router 之前注册 beforeEach。

  2. 主题色覆盖无效
    现象:修改了 modifyVars,页面仍是默认色。
    原因:babel.config.js 未把 Vant 样式路径改为 style/less,或改完未重启。
    修法:确认 style: (name) => \${name}/style/less`,修改 vue.config.js` 后重启开发服务器。

  3. Tabbar 不显示或报组件未注册
    现象:<van-tabbar> 无渲染或控制台提示未知组件。
    原因:只在页面中写了标签,未在 vant-ui.js 中 Vue.use(Tabbar/TabbarItem)。
    修法:按需引入并注册 Tabbar、TabbarItem,确认入口已引入 vant-ui.js。

  4. axios 参数位置写错
    现象:接口返回参数缺失、查询条件无效或后端收不到请求体。
    原因:查询参数误写进 data,请求体误写进 params,路径参数未拼到 URL。
    修法:查询参数用 params,请求体用 data,/article/:id 这类路径参数直接拼到 URL 后。

  5. FormData 提交失败
    现象:上传文件或表单接口收不到字段。
    原因:仍用普通对象作为 data,未使用 FormData,或字段未 append。
    修法:const fd = new FormData() 后逐项 fd.append(key, value),请求配置中 data: fd。

  6. Vuex 中直接修改 state
    现象:开启严格模式后报错,调试工具也抓不到变更。
    原因:在组件或 actions 中直接写 state.age = xxx。
    修法:统一通过 mutations 修改;组件用 commit,actions 内用 commit 调 mutation。

  7. mutation 中写异步逻辑
    现象:定时器后数据变了但调试工具状态异常或报错。
    原因:mutations 只能同步更新,异步任务放错位置。
    修法:异步逻辑放 actions,拿到结果后再 commit 对应 mutation。

  8. mapState/mapGetters 重名冲突
    现象:页面数据被覆盖或提示重复定义。
    原因:组件自身 data/computed 与映射名称相同。
    修法:映射改用对象格式重命名,如 { age: 'age' } 改为 { localAge: 'age' }。

练习 ​

  1. 给出现有 router/index.js,把登录访问守卫放到正确位置,并实现:无 token 且访问非 /login、/register 时跳转登录页。
  2. 根据接口文档写请求:GET /students 带 page、size 查询参数;DELETE /students/:id 带路径参数;POST /upload 提交 FormData。
  3. 在 Vuex 中实现:点击按钮后 3 秒把 age 改为传入值,要求不违背“mutation 是唯一修改入口”的规则,并写出组件调用代码。
  4. 组件中同时使用 mapState 映射 age,但组件自身已有 data.age,请改写映射并说明页面中如何取值。
  5. 排查一个主题色不生效项目:列出你会检查的 3 个关键文件和对应检查点。

原视频对照 ​

#视频时长
1反馈和回顾24:15
2vuex介绍09:43
3vuex安装配置08:18
4state和getters定义和直接使用15:37
5mutations更新数据11:59
6actions异步方法14:21
7借助辅助方法使用state和getters10:02

小结 ​

  1. 一级路由按“独立页面”拆分,Layout 作为首页架子承载二级内容与 Tabbar。
  2. App.vue 只保留路由出口,页面结构交给对应路由组件。
  3. 全局样式、Babel、Vue 配置类改动,改完要重启并验证实际效果。
  4. axios 写法固定,真正要判断的是参数类型:查询、请求体、路径参数、FormData。
  5. Vuex 数据修改链路应保持为:组件/actions → commit → mutation → state。
  6. 组件侧调用要区分:读 state/getters 用映射或 $store,改数据用 commit,异步用 dispatch。
  7. 辅助方法只是简化写法,遇到重名用对象格式重命名,不要与组件自身数据/计算属性冲突。