主题
属性管理模块业务介绍与开发全局组件分类业务(Day4 1/2)
本页对应 day4,共 6 段视频,约 1 小时 57 分。
学习目标
- 能够说明平台属性管理模块中一级、二级、三级分类与属性、属性值之间的业务关系,并指出分类全局组件在属性管理与 SPU 模块中的复用场景。
- 能够将三级分类封装为全局组件并完成注册,使用 Element Plus 的 Card、Form、FormItem、Select、Option 搭建一级至三级分类静态结构,并通过 inline 控制表单项布局。
- 能够为一级、二级、三级分类及平台属性接口编写 API 函数与 TypeScript 类型(Category、Attr、AttrValue 等),说明各接口所需分类 ID 参数及返回数据结构。
- 能够使用 Pinia 组合式写法创建 category 仓库,定义并暴露一级分类数据、一/二/三级分类 ID 及二/三级分类数据,完成组件挂载时获取一级分类并渲染、选择时收集 ID。
- 能够在一级、二级下拉菜单 change 事件中按需请求二级、三级分类数据,并将选中的分类 ID 与数据存入 Pinia 仓库,实现父子组件共享。
- 能够处理分类组件切换细节:一级分类变化时清空二级、三级 ID 与三级数据,二级分类变化时清空三级 ID 与数据,并在路由组件销毁时重置仓库分类数据。
知识点
业务背景:属性管理模块到底在做什么
平台属性管理不是孤立页面,它要依附“一级分类→二级分类→三级分类”的层级来展示。三级分类确定后,左侧展示该分类下已有的属性,例如“手机系统”,右侧展示属性值,例如“安卓、苹果”。后台要完成属性与属性值的增删改查。
为什么需要:属性模块和后面的 SPU 模块都会用到同一套三级分类下拉结构,如果各写一遍,维护成本高。
怎么想:先判断复用点。转写稿明确说“属性模块、SPU模块都要使用”,所以把分类部分抽成全局组件。
怎么做:在 components/category/index.vue 写组件,在入口文件注册:
ts
import Category from './components/category/index.vue'
app.component('Category', Category)易错点:Vue3 没有 Vue.component,要用 app.component;组件名和引入名不要混。
静态结构:el-form 的 inline 与 select 取值
分类组件先用 el-card、el-form、el-form-item、el-select、el-option 搭静态。转写稿调试时发现表单项竖着排,原因是 el-form-item 默认占一行;给 el-form 加 inline 后才一行放多个。
vue
<el-form inline>
<el-form-item label="一级分类">
<el-select v-model="categoryStore.c1Id">
<el-option
v-for="c1 in categoryStore.c1Arr"
:key="c1.id"
:label="c1.name"
:value="c1.id"
/>
</el-select>
</el-form-item>
</el-form>为什么需要:下拉菜单既要展示名称,又要收集 ID,后续二级、三级请求都依赖 ID。
怎么想:label 决定用户看到什么,value 决定 v-model 收集什么。
怎么做:展示 c1.name,收集 c1.id。
易错点:不要以为 v-model 收的是 label;它收的是 value。若初始值与任何 option 不匹配,会显示空白。
API 与 TS 类型:四个接口的依赖关系
转写稿把接口关系讲得很清楚:一级分类无参;二级分类要一级分类 ID;三级分类要二级分类 ID;平台属性要一级、二级、三级三个 ID。属性接口返回的是“属性对象数组”,每个属性里再有 attrValueList 属性值数组。
ts
// 一级分类
export const reqGetC1 = () => request.get<any, CategoryResponseData>('/...')
// 二级分类
export const reqGetC2 = (category1Id: number) => request.get<any, CategoryResponseData>(`/.../${category1Id}`)
// 三级分类
export const reqGetC3 = (category2Id: number) => request.get<any, CategoryResponseData>(`/.../${category2Id}`)
// 平台属性
export const reqGetAttr = (category1Id: number, category2Id: number, category3Id: number) =>
request.get<any, AttrResponseData>(`/.../${category1Id}/${category2Id}/${category3Id}`)类型上,Category 至少有 id: number、name: string;二级多 category1Id,三级多 category2Id。属性值类型可写 id?、value、attrId?;属性类型包含 id?、attrName、categoryLevel、category1Id、category2Id、attrValueList。
为什么需要:接口参数和返回结构决定组件收集哪些 ID、怎么渲染。
怎么想:先看接口文档,再写 TS,不要凭感觉猜字段。
怎么做:把一级、二级、三级和属性接口分别封装,返回类型统一约束。
易错点:新增属性与已有属性格式高度相似,但新增时通常没有 id,所以类型中 id 要允许可选;不要把一级分类类型硬套所有层级,二级、三级会多父级 ID 字段。
Pinia 仓库与联动请求:一级、二级、三级如何串起来
转写稿比较了自定义事件和 Pinia。因为一、二、三级 ID 以及数组数据会被父组件和子组件共用,放仓库更直接。
ts
export const useCategoryStore = defineStore('category', () => {
const c1Arr = ref<Category[]>([])
const c1Id = ref<number | string>('')
const c2Arr = ref<Category[]>([])
const c2Id = ref<number | string>('')
const c3Arr = ref<Category[]>([])
const c3Id = ref<number | string>('')
const getC1 = async () => {
const result = await reqGetC1()
c1Arr.value = result.data
}
// getC2、getC3 同理,分别传 c1Id、c2Id
return { c1Arr, c1Id, c2Arr, c2Id, c3Arr, c3Id, getC1 }
})组件挂载时先拿一级数据;一级下拉 change 后拿二级;二级 change 后拿三级。转写稿调试时用 change 事件,因为“选中值发生变化才触发”,重复点同一个值不会重复请求。三级确定后,父组件用仓库里的三个 ID 发平台属性请求。
为什么需要:上级分类变化后,下级数据必须重新拉取,否则会出现“手机通讯下还显示手机”的脏数据。
怎么想:一级变,清二级 ID、三级 ID、三级数组;二级变,清三级 ID、三级数组。
怎么做:在对应 change 里先清空旧值,再调仓库方法。
易错点:只清 ID 不清数组,旧下拉还会残留;组合式 Pinia 不能用 $reset,转写稿实测报错,需手动把各 ref 置空。路由切换销毁组件时,可在 onUnmounted 里清空仓库数据,避免下次进入还带上次选择。
分类全局组件的封装背景
为什么需要:平台属性管理模块和 SPU 模块都要使用“一级、二级、三级分类”的级联选择结构,如果每个页面各写一份,后续维护会重复且容易不一致。
怎么想:先把分类选择抽成全局组件,在入口文件注册,任何路由组件都能直接使用。
怎么做:在 components/Category/index.vue 写组件,在入口文件用 app.component('Category', Category) 注册。
易错点:Vue3 没有 Vue.component,要用 app.component;全局组件名要和模板中使用的一致。
ts
import Category from './components/Category/index.vue'
app.component('Category', Category)静态结构:先搭出三级下拉
为什么需要:先把结构和交互位置搭出来,才能验证后面的数据渲染与收集逻辑。
怎么想:用 el-card 包住表单,用 el-form inline 让三个表单项在一行展示;每个表单项里放 el-select 和若干 el-option。
怎么做:一级、二级、三级各一个 el-select,el-option 的 label 决定展示文字,value 决定选中后收集到的值。
易错点:el-select 收集的是 option 的 value,不是 label;如果只写 label,后续拿不到分类 ID。el-form-item 默认占一行,要加 inline 才能一行放多个。
vue
<el-card>
<el-form inline>
<el-form-item label="一级分类">
<el-select v-model="categoryStore.c1Id" @change="handleC1Change">
<el-option
v-for="c1 in categoryStore.c1Arr"
:key="c1.id"
:label="c1.name"
:value="c1.id"
/>
</el-select>
</el-form-item>
<!-- 二级、三级结构同理 -->
</el-form>
</el-card>API 与 TS 类型:按层级带参数
为什么需要:一级分类可直接查;二级分类必须知道一级 ID;三级分类必须知道二级 ID,否则接口不知道查谁下面的数据。
怎么想:先看接口文档和返回数据,再定义 getC1、getC2、getC3。一级返回数组,二级、三级对象里会多出父级 ID 字段,可复用同一个分类类型并加可选字段。
怎么做:请求函数分别接收 c1Id、c2Id,类型先按接口约束写。
易错点:转写中反复调试发现 c1Id 可能是 string | number,而接口参数要求 number,传参处要断言或处理类型;不要凭感觉猜字段,要以接口返回为准。
ts
export const reqGetC1 = () => request.get('/admin/product/getCategory1')
export const reqGetC2 = (category1Id: number) =>
request.get(`/admin/product/getCategory2/${category1Id}`)
export const reqGetC3 = (category2Id: number) =>
request.get(`/admin/product/getCategory3/${category2Id}`)用 Pinia 仓库承接分类数据
为什么需要:分类组件是子组件,属性管理页面是父组件;一级、二级、三级 ID 后续父组件也要用。用仓库比层层自定义事件更直接。
怎么想:仓库里存三组数组和三个 ID:c1Arr/c1Id、c2Arr/c2Id、c3Arr/c3Id,并提供获取一、二、三级数据的方法。
怎么做:用组合式写法 defineStore('category', () => { ... }),数据用 ref,方法内发请求并赋值,最后 return 出去。
易错点:组合式仓库必须 return 数据和函数,否则组件拿不到;$reset() 只适用于选项式 API,组合式仓库不能直接用它清空。
ts
export const useCategoryStore = defineStore('category', () => {
const c1Arr = ref<Category[]>([])
const c1Id = ref<string | number>('')
const c2Arr = ref<Category[]>([])
const c2Id = ref<string | number>('')
const c3Arr = ref<Category[]>([])
const c3Id = ref<string | number>('')
const getC1 = async () => {
const res = await reqGetC1()
c1Arr.value = res.data
}
const getC2 = async () => {
const res = await reqGetC2(c1Id.value as number)
c2Arr.value = res.data
}
const getC3 = async () => {
const res = await reqGetC3(c2Id.value as number)
c3Arr.value = res.data
}
return { c1Arr, c1Id, c2Arr, c2Id, c3Arr, c3Id, getC1, getC2, getC3 }
})获取分类数据并渲染
为什么需要:组件一挂载就应该展示一级分类;二级、三级不能一上来就请求,因为还没有父级 ID。
怎么想:onMounted 只通知仓库获取一级分类。用户选择一级后,再根据一级 ID 获取二级;选择二级后,再获取三级。
怎么做:一级 change 时调用 getC2,二级 change 时调用 getC3,三级 change 时只收集三级 ID,供父组件查询属性。
易错点:不要在挂载时把二、三级请求一起发;change 事件在选中值变化时触发,适合做“选中后再请求”。
ts
onMounted(() => {
categoryStore.getC1()
})
const handleC1Change = async () => {
categoryStore.c2Id = ''
categoryStore.c3Id = ''
categoryStore.c3Arr = []
await categoryStore.getC2()
}
const handleC2Change = async () => {
categoryStore.c3Id = ''
categoryStore.c3Arr = []
await categoryStore.getC3()
}分类全局组件细节:清空上一次数据
为什么需要:从“手机”切到“图书”时,如果不清空,上次选中的二级、三级 ID 和三级数组会残留,下拉框可能显示旧值,父组件也会拿到错误 ID。
怎么想:一级变化时,二级数据马上会被新请求覆盖,但二级 ID、三级 ID、三级数组要清;二级变化时,三级 ID 和三级数组要清。
怎么做:在对应 change 里先置空,再发下一级请求。路由组件销毁时,再把仓库里的分类数据整体清空。
易错点:清空时机要放在 change 中,不能等请求回来再清;组合式仓库不要调用 $reset(),要手动逐项置空。
ts
onBeforeUnmount(() => {
categoryStore.c1Arr = []
categoryStore.c1Id = ''
categoryStore.c2Arr = []
categoryStore.c2Id = ''
categoryStore.c3Arr = []
categoryStore.c3Id = ''
})这样,分类全局组件完成了“挂载取一级、选择取下一级、逐级收集 ID、切换清旧数据”的闭环,父组件后续只需从仓库读取三个 ID 即可继续查询属性数据。
业务目标:先看清属性管理在做什么
为什么需要:平台属性管理不是孤立页面,它要按“一级分类 → 二级分类 → 三级分类”逐级确定范围,再展示该三级分类下已有的属性与属性值。转写稿里反复强调:手机是一级,手机通讯是二级,手机是三级;三级确定后才拿属性与属性值。
怎么想:先看前台详情页的筛选结构,理解“属性”是颜色、系统等,“属性值”是红、安卓等。后台要做增删改查,但本节先完成分类全局组件、接口、TS 类型和仓库数据流转。
怎么做:把分类下拉结构封装成全局组件,因为属性管理模块和后续 SPU 模块都要用。注册方式在 Vue3 是 app.component,不是 Vue2 的 Vue.component。
易错点:不要一上来就写属性表格;先把分类联动跑通,否则拿不到 category1Id/category2Id/category3Id,属性接口无法调用。
全局分类组件静态搭建
为什么需要:属性模块和 SPU 模块都有相同的三级分类选择结构,重复写会难维护。转写稿明确说“封装成一个全局组件”。
怎么想:组件放 components/Category/index.vue,在入口文件注册为全局组件,路由组件里直接使用。
怎么做:
ts
// main.ts
import Category from './components/Category/index.vue'
app.component('Category', Category)静态结构用 el-card、el-form、el-form-item、el-select、el-option。一级、二级、三级表单项需要一行排列时,给 el-form 加 inline。
vue
<el-card>
<el-form inline>
<el-form-item label="一级分类">
<el-select v-model="categoryStore.c1Id">
<el-option
v-for="c1 in categoryStore.c1Arr"
:key="c1.id"
:label="c1.name"
:value="c1.id"
/>
</el-select>
</el-form-item>
<!-- 二级、三级同理 -->
</el-form>
</el-card>易错点:el-option 的 label 只决定展示文本,v-model 收集的是 value。转写稿中专门调试过:选择上海收集到 2,选择深圳收集到 3;如果绑定值在选项中不存在,就显示为空。
API 与 TS 类型:四个接口要分清
为什么需要:一级、二级、三级分类接口和属性接口参数不同,先定义清楚,组件里才不会乱传。
怎么想:一级不需要参数;二级需要一级 ID;三级需要二级 ID;属性与属性值需要三个 ID。转写稿说“看接口要看格式”,已有属性对象和新增属性对象格式基本一致,区别主要在是否有 id。
怎么做:
ts
// api/category.ts
export const reqGetC1 = () =>
request.get<any, CategoryResponseData>('/admin/product/getCategory1')
export const reqGetC2 = (category1Id: number) =>
request.get<any, CategoryResponseData>(`/admin/product/getCategory2/${category1Id}`)
export const reqGetC3 = (category2Id: number) =>
request.get<any, CategoryResponseData>(`/admin/product/getCategory3/${category2Id}`)
export const reqGetAttr = (
category1Id: number,
category2Id: number,
category3Id: number
) =>
request.get<any, AttrResponseData>(
`/admin/product/attrInfoList/${category1Id}/${category2Id}/${category3Id}`
)ts
export interface Category {
id: number
name: string
category1Id?: number
category2Id?: number
}
export interface CategoryResponseData {
code: number
message: string
data: Category[]
ok: boolean
}
export interface AttrValue {
id?: number
valueName: string
attrId?: number
}
export interface Attr {
id?: number
attrName: string
categoryId?: number
category1Id?: number
category2Id?: number
attrValueList: AttrValue[]
}
export interface AttrResponseData {
code: number
message: string
data: Attr[]
ok: boolean
}易错点:二级分类数据比一级多 category1Id,三级多 category2Id;TS 里可用可选属性兼容。属性值对象里的 attrId 表示它归属于哪个属性。
Pinia 仓库管理分类数据与联动
为什么需要:分类组件是子组件,属性区域在父组件;三级 ID 要给父组件发属性请求。转写稿比较了自定义事件、props 和仓库,最后选择 Pinia:数据放仓库,父子都能取。
怎么想:一级挂载时请求;一级 change 时清空二级、三级旧数据并请求二级;二级 change 时清空三级并请求三级。每次选择都把 ID 收集到仓库。
怎么做:
ts
export const useCategoryStore = defineStore('category', () => {
const c1Arr = ref<Category[]>([])
const c1Id = ref<string | number>('')
const c2Arr = ref<Category[]>([])
const c2Id = ref<string | number>('')
const c3Arr = ref<Category[]>([])
const c3Id = ref<string | number>('')
const getC1 = async () => {
const res = await reqGetC1()
c1Arr.value = res.data
}
const getC2 = async () => {
const res = await reqGetC2(c1Id.value as number)
c2Arr.value = res.data
}
const getC3 = async () => {
const res = await reqGetC3(c2Id.value as number)
c3Arr.value = res.data
}
return { c1Arr, c1Id, c2Arr, c2Id, c3Arr, c3Id, getC1, getC2, getC3 }
})组件中:
ts
onMounted(() => {
categoryStore.getC1()
})
const handleC1Change = () => {
categoryStore.c2Id = ''
categoryStore.c3Id = ''
categoryStore.c3Arr = []
categoryStore.getC2()
}
const handleC2Change = () => {
categoryStore.c3Id = ''
categoryStore.getC3()
}易错点:切换一级时必须清空二级 ID、三级 ID 和三级数组;切换二级时清空三级 ID。转写稿调试时发现,不清空会出现“二级数据没了但 ID 还在”的错位。组合式 Pinia 中 $reset 不生效,转写稿验证后改为手动清空;$reset 只适用于选项式写法。
路由切换与销毁清空
为什么需要:从属性管理切到其他路由再回来,仓库数据还在,容易残留上一次选择。
怎么想:路由组件切换会销毁子组件,可在 onUnmounted 清空仓库分类数据。
怎么做:
ts
onUnmounted(() => {
categoryStore.c1Arr = []
categoryStore.c2Arr = []
categoryStore.c3Arr = []
categoryStore.c1Id = ''
categoryStore.c2Id = ''
categoryStore.c3Id = ''
})易错点:不要误以为组合式仓库能直接 $reset;要么手动清,要么改用选项式写法。父组件后续用三个 ID 调 reqGetAttr 展示属性与属性值,所以分类 ID 的收集与清空必须先稳定。
课堂画面






代码示例
示例 1:分类全局组件的静态搭建(el-card + el-form inline + 三级 el-select)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>01-分类全局组件静态搭建</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/element-plus"></script>
</head>
<body>
<div id="app">
<!-- 相当于 components/category/index.vue 里的模板 -->
<el-card>
<!-- inline:让多个 el-form-item 在一行里横向排布 -->
<el-form inline>
<el-form-item label="一级分类">
<el-select v-model="c1Id" placeholder="请选择" style="width: 200px">
<el-option
v-for="item in c1Arr"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="二级分类">
<el-select v-model="c2Id" placeholder="请选择" style="width: 200px">
<el-option
v-for="item in c2Arr"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="三级分类">
<el-select v-model="c3Id" placeholder="请选择" style="width: 200px">
<el-option
v-for="item in c3Arr"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</el-card>
<p style="margin-top: 16px">收集到的值:{{ c1Id }} / {{ c2Id }} / {{ c3Id }}</p>
</div>
<script>
const { createApp, ref } = Vue;
const app = createApp({
setup() {
// 静态数据先占位,后面换成仓库里的真实数据
const c1Arr = ref([
{ id: 1, name: '图书' },
{ id: 2, name: '手机' },
{ id: 3, name: '数码' },
]);
const c2Arr = ref([
{ id: 13, name: '手机通讯' },
{ id: 14, name: '运营商' },
{ id: 15, name: '手机配件' },
]);
const c3Arr = ref([
{ id: 61, name: '手机' },
{ id: 62, name: '对讲机' },
]);
// v-model 绑定的值:收集的是 option 的 value(ID),不是 label
const c1Id = ref(2);
const c2Id = ref(13);
const c3Id = ref(61);
return { c1Arr, c2Arr, c3Arr, c1Id, c2Id, c3Id };
},
});
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>要点与易错点:
el-form必须加inline,否则每个el-form-item会像表格项一样独占一行,三级下拉会竖着排下来——转写稿里"因滥属性"说的就是这个inline。el-select收集数据写在v-model上,收集的是选中el-option的value,不是label;label只决定用户看到的内容。所以这里:value="item.id"绑定 ID,因为后面发二级、三级请求都要拿一级、二级的 ID。v-model是双向绑定:把c1Id初始值写成2会自动选中"手机";写成下拉里不存在的值(如8)则显示空白;写成空串''就显示 placeholder。:key="item.id"不要用 index,避免后续数据变化时复用错乱。
示例 2:Pinia 仓库(组合式写法)管理三级分类数据 + 三级联动收集 ID
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>02-分类全局组件业务:Pinia 仓库 + 三级联动</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/element-plus"></script>
<script src="https://unpkg.com/pinia"></script>
</head>
<body>
<div id="app">
<el-card>
<el-form inline>
<el-form-item label="一级分类">
<el-select
v-model="categoryStore.c1Id"
placeholder="请选择"
style="width: 200px"
@change="handleC1Change"
>
<el-option
v-for="item in categoryStore.c1Arr"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="二级分类">
<el-select
v-model="categoryStore.c2Id"
placeholder="请选择"
style="width: 200px"
@change="handleC2Change"
>
<el-option
v-for="item in categoryStore.c2Arr"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="三级分类">
<el-select
v-model="categoryStore.c3Id"
placeholder="请选择"
style="width: 200px"
>
<el-option
v-for="item in categoryStore.c3Arr"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
</el-card>
<el-card style="margin-top: 16px">
<p>一级分类ID:{{ categoryStore.c1Id }}</p>
<p>二级分类ID:{{ categoryStore.c2Id }}</p>
<p>三级分类ID:{{ categoryStore.c3Id }}</p>
</el-card>
</div>
<script>
const { createApp, ref, onMounted } = Vue;
const { createPinia, defineStore } = Pinia;
/* ===== 模拟后端接口:真实项目里就是 request.get('/admin/product/getCategory1') 等 ===== */
const DB = {
c1: [
{ id: 1, name: '图书' },
{ id: 2, name: '手机' },
{ id: 3, name: '数码' },
],
c2: {
1: [{ id: 11, name: '文学' }, { id: 12, name: '历史' }],
2: [{ id: 13, name: '手机通讯' }, { id: 14, name: '运营商' }, { id: 15, name: '手机配件' }],
3: [{ id: 21, name: '摄影摄像' }],
},
c3: {
11: [{ id: 111, name: '小说' }],
12: [{ id: 121, name: '通史' }],
13: [{ id: 61, name: '手机' }, { id: 62, name: '对讲机' }],
14: [{ id: 63, name: '合约机' }],
15: [{ id: 64, name: '充电器' }],
21: [{ id: 211, name: '单反相机' }],
},
};
const reqGetC1 = () =>
new Promise((resolve) => setTimeout(() => resolve(DB.c1), 100));
// 二级分类需要一级分类的 ID
const reqGetC2 = (c1Id) =>
new Promise((resolve) => setTimeout(() => resolve(DB.c2[c1Id] || []), 100));
// 三级分类需要二级分类的 ID
const reqGetC3 = (c2Id) =>
new Promise((resolve) => setTimeout(() => resolve(DB.c3[c2Id] || []), 100));
/* ===== 仓库:组合式 API 写法(setup 函数 + return) ===== */
const useCategoryStore = defineStore('category', () => {
const c1Arr = ref([]);
const c1Id = ref('');
const c2Arr = ref([]);
const c2Id = ref('');
const c3Arr = ref([]);
const c3Id = ref('');
// 一级分类数据
const getC1 = async () => {
c1Arr.value = await reqGetC1();
};
// 二级分类数据:必须带上一级分类 ID
const getC2 = async (id) => {
c2Arr.value = await reqGetC2(id);
};
// 三级分类数据:必须带上二级分类 ID
const getC3 = async (id) => {
c3Arr.value = await reqGetC3(id);
};
// 不 return 出去,组件里就拿不到
return { c1Arr, c1Id, c2Arr, c2Id, c3Arr, c3Id, getC1, getC2, getC3 };
});
const app = createApp({
setup() {
const categoryStore = useCategoryStore();
// 组件挂载时只捞一级分类
onMounted(() => {
categoryStore.getC1();
});
// 一级分类下拉 change:清空上一次的二级ID、三级ID、三级数组,再请求二级
const handleC1Change = () => {
categoryStore.c2Id = '';
categoryStore.c3Id = '';
categoryStore.c3Arr = [];
categoryStore.getC2(categoryStore.c1Id);
};
// 二级分类下拉 change:清空上一次的三级ID,再请求三级
const handleC2Change = () => {
categoryStore.c3Id = '';
categoryStore.getC3(categoryStore.c2Id);
};
return { categoryStore, handleC1Change, handleC2Change };
},
});
app.use(createPinia());
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>要点与易错点:
- 仓库用组合式写法时,所有数据和方法都必须
return出去,否则组件里拿不到(转写稿反复强调"不 return 组件用不了")。 - 请求时机:
onMounted里只发一级分类请求;二级要等一级选中(@change)才发;三级要等二级选中才发——因为二级接口要一级 ID、三级接口要二级 ID。 - 用
@change而不是watch监听,好处是选中值真正变化时才触发,重复点同一个值不会重复发请求。 handleC1Change里必须先清空c2Id、c3Id、c3Arr再请求二级:c2Arr不用手动清,请求回来会覆盖;但c2Id不清就会出现"换了二级分类数据,下拉框里却还挂着上一次的 ID",因为v-model没有匹配项时那一栏就显示空白。- 注意接口层级:一级分类返回
{ id, name },二级多一个category1Id,三级再多一个category2Id,同一个类型可以用可选字段(?)兼容三级。
示例 3:组件销毁时清空仓库数据(路由切换场景)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>03-销毁时清空仓库数据</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/element-plus"></script>
<script src="https://unpkg.com/pinia"></script>
</head>
<body>
<div id="app">
<el-button type="primary" @click="visible = !visible">
{{ visible ? '离开属性管理页(模拟路由切换)' : '进入属性管理页' }}
</el-button>
<category-page v-if="visible" />
<el-card style="margin-top: 16px">
<pre>仓库快照:{{ snapshot }}</pre>
</el-card>
</div>
<script>
const { createApp, ref, computed, onMounted, onBeforeUnmount } = Vue;
const { createPinia, defineStore } = Pinia;
const reqGetC1 = () =>
new Promise((resolve) =>
setTimeout(
() =>
resolve([
{ id: 1, name: '图书' },
{ id: 2, name: '手机' },
]),
100
)
);
/* ===== 仓库:组合式 API 写法 ===== */
const useCategoryStore = defineStore('category', () => {
const c1Arr = ref([]);
const c1Id = ref('');
const c2Arr = ref([]);
const c2Id = ref('');
const c3Arr = ref([]);
const c3Id = ref('');
const getC1 = async () => {
c1Arr.value = await reqGetC1();
};
return { c1Arr, c1Id, c2Arr, c2Id, c3Arr, c3Id, getC1 };
});
/* ===== 分类全局组件(对应 components/category/index.vue) ===== */
const CategoryPage = {
setup() {
const categoryStore = useCategoryStore();
onMounted(() => {
categoryStore.getC1();
});
// 路由切换 → 路由组件销毁 → 子组件跟着销毁,在这里清空仓库
onBeforeUnmount(() => {
categoryStore.c1Arr = [];
categoryStore.c1Id = '';
categoryStore.c2Arr = [];
categoryStore.c2Id = '';
categoryStore.c3Arr = [];
categoryStore.c3Id = '';
});
return { categoryStore };
},
template: `
<el-card style="margin-top: 16px">
<el-form inline>
<el-form-item label="一级分类">
<el-select v-model="categoryStore.c1Id" placeholder="请选择" style="width: 200px">
<el-option
v-for="item in categoryStore.c1Arr"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="二级分类">
<el-select v-model="categoryStore.c2Id" placeholder="请选择" style="width: 200px">
<el-option
v-for="item in categoryStore.c2Arr"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="三级分类">
<el-select v-model="categoryStore.c3Id" placeholder="请选择" style="width: 200px">
<el-option常见错误
- 现象:模板里用
<category />不渲染或报未注册 → 原因:全局组件未在入口app.component注册,或注册名与模板名不一致 → 修法:入口统一注册,检查注册名、模板名、文件路径拼写。 - 现象:选了一级分类,二级请求参数仍是旧 ID 或
undefined→ 原因:v-model绑到了label或局部字段,未收集value/ID,类型number|string未收窄 → 修法:el-option :value绑 ID,v-model绑 store 中对应 ID,发请求前判空并统一类型。 - 现象:切换一级分类后,三级下拉仍显示上次选项/残留 ID → 原因:只更新二级数据,未清空
C2ID、C3ID、三级数组 → 修法:一级change先清二三级 ID 和三级数组,再拉二级;二级change清三级 ID 和数组再拉三级。 - 现象:组合式 Pinia store 调
$reset报错 → 原因:$reset只适用于选项式 store → 修法:组合式 store 显式重置各ref/数组,或改用选项式写法。 - 现象:一进页面就发二级/三级请求,参数为空或重复请求 → 原因:请求写在
onMounted或watch初值,未按依赖变化触发 → 修法:挂载只拉一级,二级由一级change触发,三级由二级change触发。 - 现象:仓库里已有数据,组件却拿不到 → 原因:组合式 store 返回对象漏写字段/方法,或解构后失去响应性 → 修法:
return全部需要字段,使用storeToRefs或直接store.xxx,不要直接解构。 - 现象:属性接口 TS 报字段不存在或 ID 类型不匹配 → 原因:复用分类类型时可选字段未处理,接口参数
string/number混用 → 修法:可选字段标?,参数统一类型,调用处显式转换并判空。 - 现象:同一分类反复点击仍发请求 → 原因:用
click代替change,或未判断值是否变化 → 修法:使用change事件,必要时比较新旧值,避免重复请求。
练习
- 将
category封装为全局组件并在入口注册,在属性管理和 SPU 两个路由组件中复用,验证注册名与模板名一致。 - 完成三级联动:一级挂载拉取,一级
change拉二级,二级change拉三级;把C1ID/C2ID/C3ID存入 Pinia,并让父组件可读取。 - 增加切换清空逻辑:一级变化清
C2ID/C3ID/C3 数组;二级变化清C3ID/C3 数组;验证不会用旧 ID 发请求。 - 为属性接口补齐 TS 类型与请求函数,父组件拿到三级 ID 后请求属性列表并渲染;ID 为空时不发请求。
- 增加路由离开重置:在属性组件销毁时重置分类 store(组合式写法显式重置),再次进入验证无残留数据。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 属性管理模块业务的介绍 | 05:14 |
| 2 | 全局组件分类的静态的搭建 | 10:22 |
| 3 | 书写属性管理模块API与数据ts类型 | 40:30 |
| 4 | pinia仓库管理数据之一级分类 | 26:23 |
| 5 | 获取分类全局组件二级、三级分类的数据并且收集数据的分类的id | 18:30 |
| 6 | 完成属性管理模块分类全局组件的业务 | 16:29 |
小结
- 分类组件是跨模块复用资产,注册、命名、数据来源必须统一。
- 联动请求由依赖变化驱动,二三级请求不能抢跑。
- 分类 ID 只保留一份可信来源,展示与接口参数都从它取。
- 切换上级必须清下级,清 ID 与清展示数据要成对。
- 组合式 Pinia 的重置要显式实现,不要照搬选项式 API。
- 先定 API 与 TS 类型再写页面,能减少参数和字段类型问题。
- 用
change事件和条件判断控制请求频率,避免重复请求。 - 调试时先核对 store 中的 ID、请求参数、接口返回三者是否一致。