主题
搭建属性与属性值的静态与完成添加新的属性与属性值业务(Day4 2/2)
本页对应 day4,共 5 段视频,约 1 小时 37 分。
学习目标
- 能够使用 Card、Button、Table、TableColumn、Tag、Empty 等组件完成“属性与属性值”区域静态结构搭建,并通过插槽正确渲染属性值列表、操作按钮和空状态。
- 能够监听三级分类 ID 变化,在有效分类下发起获取属性与属性值请求,完成数据存储、表格渲染、分类切换时清空旧数据,并实现添加按钮的禁用控制。
- 能够使用 Vue 内置
Transition组件为元素、组件或路由切换添加进入/离开过渡动画,正确配置v-if/v-show、name及过渡类名。 - 能够通过场景状态实现“展示列表 / 添加或修改属性”之间的切换,完成添加、编辑、取消事件绑定及父子组件场景值传递。
- 能够查看并说明添加/更新属性接口的请求参数,区分新增与更新时
attrId等字段差异,并编写可复用的 API 请求函数及对应 TypeScript 类型。
知识点
静态骨架:卡片、按钮与表格
为什么需要:平台属性管理底部要展示“某个三级分类下已有的属性与属性值”,先把结构搭出来,后续才能接数据。
怎么想:底部是一个卡片区域,内部先放“添加平台属性”按钮和表格;表格列包括序号、属性名称、属性值、操作。
怎么做:在 views/product/Attr 路由组件中写静态结构,按钮图标从 @element-plus/icons-vue 引入 Plus。
vue
<el-card>
<el-button type="primary" :icon="Plus" :disabled="!categoryStore.c3Id">
添加平台属性
</el-button>
<el-table :data="attrList" border style="margin: 10px 0">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="属性名称" width="120" prop="attrName" />
<el-table-column label="属性值">
<template #default="{ row }">
<el-tag
v-for="item in row.attrValueList"
:key="item.id"
:type="item.id % 2 === 0 ? 'primary' : 'danger'"
style="margin: 5px"
>
{{ item.valueName }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="warning" size="small" :icon="Edit">编辑</el-button>
<el-button type="danger" size="small" :icon="Delete">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>易错点:el-table-column 不要拼错;属性值要用插槽渲染,因为每个属性对应多个属性值;按钮在未选三级分类时应禁用。
获取数据:监听三级分类 ID
为什么需要:接口需要一级、二级、三级分类 ID,只有三级分类确定后,才能查该分类下的属性。
怎么想:三级分类 ID 存在仓库里,监听 c3Id 变化;有值就发请求,没有值就清空表格。
怎么做:从仓库取 c1Id/c2Id/c3Id,封装 getAttrList,因为添加成功后还要重新拉取。
ts
const categoryStore = useCategoryStore()
const attrList = ref<Attr[]>([])
const getAttrList = async () => {
const { c1Id, c2Id, c3Id } = categoryStore
const res = await reqAttrList(c1Id, c2Id, c3Id)
attrList.value = res.data
}
watch(
() => categoryStore.c3Id,
(val) => {
attrList.value = []
if (val) getAttrList()
}
)易错点:只判断 c3Id 变化还不够,必须判断它不为空串;切换分类时要先清空旧数据,否则会短暂显示上一个分类的属性。类型上注意仓库里的 ID 可能是 string | number。
渲染与空状态
为什么需要:有数据展示表格,无数据展示空状态,界面才合理。
怎么想:用 v-if="attrList.length" 展示表格,否则展示 el-empty。
怎么做:属性值用 el-tag 遍历 row.attrValueList,显示 item.valueName;空状态可自定义图片和描述。
vue
<el-table v-if="attrList.length" :data="attrList">...</el-table>
<el-empty v-else description="暂无数据" />易错点:不要写成 attrList.length > 0 与 v-else 混用导致结构混乱;el-empty 的图片地址要确保有效。
场景切换:添加与修改的静态结构
为什么需要:点击“添加平台属性”或“编辑”后,底部卡片要从表格切换到表单。
怎么想:用 scene 控制二选一,0 显示表格,1 显示表单。
怎么做:表单用 el-form :inline="true",内部放属性名称输入框;下方放“添加属性值”“取消”按钮。
ts
const scene = ref(0)
const addAttr = () => { scene.value = 1 }
const updateAttr = () => { scene.value = 1 }vue
<div v-show="scene === 0">表格区域</div>
<div v-show="scene === 1">
<el-form :inline="true">
<el-form-item label="属性名称">
<el-input placeholder="请输入属性名称" />
</el-form-item>
</el-form>
<el-button type="primary" :icon="Plus">添加属性值</el-button>
<el-button @click="scene = 0">取消</el-button>
</div>易错点:取消时把 scene 赋值为 0,不要写成比较;表单不要误放进 inline 的 el-form-item 内部,否则布局会挤在一起。
过渡动画与版本提醒
为什么需要:场景切换加过渡更自然;同时 Element Plus 版本过低会出现“已禁用下拉仍能展开”的问题。
怎么想:过渡动画要求元素有 v-if/v-show,外层包 transition,Vue3 类名为 v-enter-from、v-enter-active、v-enter-to。
怎么做:可给场景区域包 transition,但多个元素动画要用 name 区分类名。
易错点:Vue3 不要写 Vue2 的 v-enter;若遇到禁用失效,先检查 Element Plus 版本,必要时升级到较新版本。
底部结构切换:先分清父子与“二选一”
为什么需要:平台属性管理页面上半部分是三级分类,下半部分要展示该分类下已有属性与属性值;点击“添加平台属性”后,底部又要切换成新增表单。怎么想:底部卡片不是同时展示两块内容,而是用场景值在“列表态”和“编辑态”之间二选一。怎么做:在 Attr 路由组件里定义 scene,例如 0 表示展示已有属性表格,1 表示展示添加/修改表单;用 v-show 或 v-if 控制两块结构。添加、编辑按钮把 scene 置为 1,取消按钮归零。易错点:取消时要用赋值 scene = 0,不要写成比较;表单区不要随手塞进行内表单内部,否则布局会挤在一起。
展示已有属性与属性值:监听三级分类 ID 再发请求
为什么需要:接口需要一级、二级、三级分类 ID,只有三级分类确定后,才能拿到对应属性列表。怎么想:不要一挂载就发请求,因为此时三级分类可能还没有值;应监听仓库中的三级分类 ID,有值再请求。怎么做:从分类仓库取出 C1、C2、C3,监听 C3 变化;请求前判断 C3 不能为空串。把“获取已有属性与属性值”封装成函数,因为添加成功后还要重新拉取列表。拿到数据后存入 attrList,表格展示 attrName,属性值用标签循环渲染 valueName。易错点:只监听 C3 变化还不够,清空分类时也会触发请求但参数为空;所以必须加非空判断。切换分类时还要先清空旧 attrList,否则会短暂显示上一个分类的数据。
空状态与交互细节:清空、禁用、场景切换
为什么需要:没有数据时不应继续展示空表格;未选三级分类时也不应允许添加属性。怎么想:有数据展示表格,无数据展示空状态;添加按钮是否禁用取决于 C3 是否存在。怎么做:用 v-if="attrList.length" 展示表格,否则展示 el-empty;给添加按钮绑定 disabled,根据 categoryStore.C3Id 做布尔判断。表单里的分类下拉在 scene === 1 时禁用,场景值由父组件传给子组件。易错点:禁用值要是布尔值,不要直接塞数字或字符串;旧版 Element Plus 可能出现“看起来禁用但仍能展开”的问题,转写稿中通过升级依赖版本解决。工作时要先看项目依赖版本,再对照对应版本文档。
添加/更新参数:先看接口再收集数据
为什么需要:新增属性不是只收一个名字,还要收集属性值数组,并且参数格式必须符合接口。怎么想:新增和更新共用一个保存接口,区别在“有没有 id”:新增没有 id,更新时属性对象、属性值对象要多带 id。怎么做:按笔记保留参数格式:
json
{
"categoryId":61,
"categoryLevel": 3,
"attrName": "豪哥",
"attrValueList": [
{
"valueName": "18"
}
],
}更新多一些 id。categoryId 是三级分类 ID,categoryLevel 用于区分分类层级;新增属性值只需要 valueName,更新时再按接口补属性 id 与属性值 id。易错点:不要凭感觉造字段,先看接口文档和 Network 请求体;新增成功后接口可能没有返回数据,不能假设一定有结果;添加成功后要重新调用列表请求。
过渡动画:有条件再写类名
为什么需要:场景切换和路由切换加过渡更自然,但过渡动画不是随便套个标签就生效。怎么想:元素或组件必须有进入/离开状态,外层要用 transition 包裹。怎么做:用 v-if/v-show 制造显示隐藏,外层套 transition;Vue3 进入起始类名是 v-enter-from,进入结束是 v-enter-to,过程用 v-enter-active;离开用 v-leave-from、v-leave-to、v-leave-active。多个元素动画可给 name 区分类名。易错点:v-if 与 v-else 之间不要夹无关内容;Vue3 兼容部分 Vue2 写法,但新项目应按 Vue3 类名写;动画时间、位移、缩放要放在 active 类里。
课堂画面





代码示例
示例一:底部静态搭建 + 展示已有属性与属性值(含空状态与过渡动画)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>平台属性 - 底部静态与已有属性展示</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/@element-plus/icons-vue"></script>
<style>
/* transition name="attr" 对应的类名(Vue3 是 xxx-enter-from) */
.attr-enter-from { opacity: 0; transform: scale(0.5); }
.attr-enter-active { transition: all 0.5s; }
.attr-enter-to { opacity: 1; transform: scale(1); }
.attr-leave-from { opacity: 1; transform: scale(1); }
.attr-leave-active { transition: all 0.5s; }
.attr-leave-to { opacity: 0; transform: scale(0.5); }
</style>
</head>
<body>
<div id="app">
<el-card>
<!-- 顶部按钮:没选三级分类就禁用 -->
<el-button
type="primary"
:icon="Plus"
:disabled="!categoryStore.c3Id"
@click="addAttr"
>添加平台属性</el-button>
<!-- 底部:有数据展示表格,没数据展示空状态,二选一 + 过渡动画 -->
<transition name="attr" mode="out-in">
<el-table
v-if="attrList.length > 0"
border
:data="attrList"
style="margin: 10px 0"
>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="属性名称" width="120" prop="attrName" />
<el-table-column label="属性值">
<template #default="{ row }">
<el-tag
v-for="item in row.attrValueList"
:key="item.valueName"
:type="item.id % 2 === 0 ? 'primary' : 'danger'"
style="margin: 5px"
>{{ item.valueName }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button type="warning" size="small" :icon="Edit" @click="updateAttr(row)" />
<el-button type="danger" size="small" :icon="Delete" />
</template>
</el-table-column>
</el-table>
<!-- image 属性可换成自己的图片地址,image-size 控制大小 -->
<el-empty v-else description="暂无数据" :image-size="200" />
</transition>
</el-card>
</div>
<script>
const { createApp, ref, watch } = Vue;
const { Plus, Edit, Delete } = ElementPlusIconsVue;
createApp({
setup() {
// 模拟小仓库里的三个分类 ID
const categoryStore = ref({ c1Id: 1, c2Id: 2, c3Id: 61 });
const attrList = ref([]);
// 模拟接口返回:属性对象里套着 attrValueList
const mockList = [
{ id: 1, attrName: '手机', attrValueList: [{ id: 11, valueName: '18' }, { id: 12, valueName: '20' }] },
{ id: 2, attrName: '通讯', attrValueList: [{ id: 13, valueName: '4G' }] }
];
// 获取全部已有属性与属性值(封装成函数:添加/修改成功后还要再调一次)
const getAttrList = async () => {
// 真实项目:const res = await reqAttrList(c1Id, c2Id, c3Id)
attrList.value = mockList;
};
// 监听三级分类 ID:有值才发请求,且每次变化先清空
watch(
() => categoryStore.value.c3Id,
(val) => {
attrList.value = []; // 先清空,避免残留上一个分类的数据
if (val) getAttrList(); // 为空串说明参数不全,不发请求
},
{ immediate: true }
);
const addAttr = () => {};
const updateAttr = (row) => {};
return { categoryStore, attrList, Plus, Edit, Delete, addAttr, updateAttr };
}
}).use(ElementPlus).mount('#app');
</script>
</body>
</html>关键行与易错点
- 表格四列:第一列
type="index"是序号(不是 ID),width="80"+align="center";属性名称width="120";「属性值」和「操作」不写 width,剩余空间自动分配。 style="margin: 10px 0"是给按钮和表格之间留间距,注意单位别丢。- 第三列必须用插槽
#default="{ row }",因为要在里面v-for遍历row.attrValueList,再用el-tag渲染item.valueName。 el-tag的 type 按item.id % 2动态取色:偶数primary、奇数danger;注意新增的属性值没有 id,undefined % 2是NaN,不会等于 0,会走danger,所以这种写法只适合“展示已有数据”的场景。watch必须加两个判断:有值才发请求(三级分类为空串时参数不全,接口会报错),变化时先清空列表,否则切换分类后旧数据还挂在那儿。v-if/v-else二选一 +<transition name="attr">才有过渡动画;过渡类名在 Vue3 是.attr-enter-from、.attr-enter-active、.attr-enter-to、.attr-leave-from、.attr-leave-active、.attr-leave-to。- 按钮
:disabled="!categoryStore.c3Id"必须转成布尔值,不要直接给 ID 或空串。
示例二:底部卡片结构切换(场景 0 展示 / 场景 1 添加与修改)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>平台属性 - 场景切换</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/@element-plus/icons-vue"></script>
</head>
<body>
<div id="app">
<el-card>
<!-- 子组件:三级分类,父组件把 scene 传下去 -->
<category-select :scene="scene" />
<!-- 场景 0:展示已有属性与属性值 -->
<div v-show="scene === 0">
<el-button type="primary" :icon="Plus" @click="addAttr">添加平台属性</el-button>
<el-table border :data="attrList" style="margin: 10px 0">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="常见错误
一、二级分类都没选完就发出参数残缺的请求,或清空分类时仍发请求 现象:Network 面板里出现
c1Id/c2Id为空的请求,或反复请求报错。 原因:只监听了c3Id的变化就无条件发请求,没有判断值是否为空串。 修法:监听回调里加if (c3Id)前置判断,只有三级分类真正有值时才调用getAttrList()。切换分类后底部仍显示上一个分类的属性与属性值 现象:从“手机”切到别的分类,表格里还是手机的数据,甚至和空状态同时出现。 原因:只做了“有值就重新赋值”,没做“无值/变化时先清空”。 修法:监听到
c3Id变化的第一件事就是attrArr.value = [],再按条件请求并回填。过渡动画完全不生效 现象:写了
.v-enter-from之类的样式,元素进出毫无动画。 原因:元素没有v-if/v-show,或transition与v-if之间夹了注释/其他节点,或 Vue3 里照抄了 V2 的.v-enter、.v-leave类名。 修法:保证被包裹元素有v-if/v-show且是transition的唯一子节点;类名用v-enter-from/v-enter-active/v-enter-to(离开同理)。页面上多个过渡动画互相“串台” 现象:给表格和给路由各写一套动画,结果样式互相覆盖。 原因:多个
transition都没设name,类名全是默认的v-*。 修法:给每个transition加name="attr",样式改写为.attr-enter-from等。下拉菜单明明
disabled了,鼠标一放还能展开 现象:分类选择器显示灰色,但悬停仍弹出面板。 原因:Element Plus 版本过低存在该缺陷。 修法:卸载后安装最新版element-plus,重装依赖再跑;同时养成“先看版本号再查文档”的习惯。保存时把新增属性值的
id也带上了 现象:新增属性值提交后报错,或后端把新增项当成了修改。 原因:直接复用了“已有属性”的 TS 类型去拼参数,没区分新增与更新。 修法:把id、attrId、valueId等字段在类型里标成可选,收集新增数据时只保留valueName。用
=判断场景值导致切换失效 现象:点取消回不到表格,或场景永远停在一个状态。 原因:写成scene = 0(赋值)而非scene = 0的比较,且条件分支没写全。 修法:统一用===比较,v-show="scene === 0"/v-show="scene === 1"成对出现。
练习
- 给底部卡片加上“无数据”占位:当
attrArr.length === 0时展示空状态(自定义图片 + 文案),否则展示表格,二者严格二选一。 - 为表格的进出场各写一套过渡动画(进入时透明度 0→1、离开时缩放 1→0.6),用
name与路由组件的过渡动画区分开,互不干扰。 - 把“添加属性值”按钮做成“未选三级分类时禁用”的效果,并说明为什么这里必须传布尔值而不是数字或字符串。
- 在点击“编辑”时把当前行的属性对象传给父组件,父组件据此把场景值切到添加/修改页;点击“取消”后切回表格,并检查是否残留了上一次编辑的数据。
- 手写一次新增属性的请求参数(属性名 + 一个属性值),然后在 Network 里核对:
categoryId、categoryLevel、attrName、attrValueList[].valueName是否齐全,新增的属性值是否没有id。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 完成属性与属性值静态的搭建 | 07:26 |
| 2 | 展示有的属性与属性值 | 32:38 |
| 3 | 拓展过渡动画效果 | 18:47 |
| 4 | 完成场景的切换 | 27:21 |
| 5 | 添加属性与更新数据接口的查看与参数的理解 | 11:12 |
小结
- 底部卡片的“表格 / 表单”是二选一关系,用一个数值型
scene控制即可,比布尔值更利于将来扩展第三种场景。 - 监听三级分类 ID 是发请求的唯一触发点,但必须同时满足“值变化”和“值非空”两个条件。
- 分类变化时先清空再请求,是避免脏数据残留的关键一步,顺序不能反。
- 请求函数要单独封装而不是写死在监听里,因为“新增成功后重新拉取列表”还要复用它。
- 过渡动画三要素:元素有
v-if/v-show、外层套transition、类名用 Vue3 的-from/-active/-to三段式;多套动画必须靠name隔离。 - 新增与更新共用同一个接口,区别只在参数里有没有
id,所以不要写 if/else 分支判断请求方法。 - 组件库的
disabled失效等问题优先怀疑版本,遇到异常先npm view对一下版本号再查文档。 - 这类后台模块的通用套路是“结构切换 → 拉数据 → 收集参数 → 提交 → 重新拉取”,掌握一次即可迁移到 SPU、SKU 等模块。