Skip to content

搭建属性与属性值的静态与完成添加新的属性与属性值业务(Day4 2/2) ​

本页对应 day4,共 5 段视频,约 1 小时 37 分。

学习目标 ​

  1. 能够使用 Card、Button、Table、TableColumn、Tag、Empty 等组件完成“属性与属性值”区域静态结构搭建,并通过插槽正确渲染属性值列表、操作按钮和空状态。
  2. 能够监听三级分类 ID 变化,在有效分类下发起获取属性与属性值请求,完成数据存储、表格渲染、分类切换时清空旧数据,并实现添加按钮的禁用控制。
  3. 能够使用 Vue 内置 Transition 组件为元素、组件或路由切换添加进入/离开过渡动画,正确配置 v-if/v-show、name 及过渡类名。
  4. 能够通过场景状态实现“展示列表 / 添加或修改属性”之间的切换,完成添加、编辑、取消事件绑定及父子组件场景值传递。
  5. 能够查看并说明添加/更新属性接口的请求参数,区分新增与更新时 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 类里。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:底部静态搭建 + 展示已有属性与属性值(含空状态与过渡动画) ​

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="

常见错误 ​

  1. 一、二级分类都没选完就发出参数残缺的请求,或清空分类时仍发请求 现象:Network 面板里出现 c1Id/c2Id 为空的请求,或反复请求报错。 原因:只监听了 c3Id 的变化就无条件发请求,没有判断值是否为空串。 修法:监听回调里加 if (c3Id) 前置判断,只有三级分类真正有值时才调用 getAttrList()。

  2. 切换分类后底部仍显示上一个分类的属性与属性值 现象:从“手机”切到别的分类,表格里还是手机的数据,甚至和空状态同时出现。 原因:只做了“有值就重新赋值”,没做“无值/变化时先清空”。 修法:监听到 c3Id 变化的第一件事就是 attrArr.value = [],再按条件请求并回填。

  3. 过渡动画完全不生效 现象:写了 .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(离开同理)。

  4. 页面上多个过渡动画互相“串台” 现象:给表格和给路由各写一套动画,结果样式互相覆盖。 原因:多个 transition 都没设 name,类名全是默认的 v-*。 修法:给每个 transition 加 name="attr",样式改写为 .attr-enter-from 等。

  5. 下拉菜单明明 disabled 了,鼠标一放还能展开 现象:分类选择器显示灰色,但悬停仍弹出面板。 原因:Element Plus 版本过低存在该缺陷。 修法:卸载后安装最新版 element-plus,重装依赖再跑;同时养成“先看版本号再查文档”的习惯。

  6. 保存时把新增属性值的 id 也带上了 现象:新增属性值提交后报错,或后端把新增项当成了修改。 原因:直接复用了“已有属性”的 TS 类型去拼参数,没区分新增与更新。 修法:把 id、attrId、valueId 等字段在类型里标成可选,收集新增数据时只保留 valueName。

  7. 用 = 判断场景值导致切换失效 现象:点取消回不到表格,或场景永远停在一个状态。 原因:写成 scene = 0(赋值)而非 scene = 0 的比较,且条件分支没写全。 修法:统一用 === 比较,v-show="scene === 0" / v-show="scene === 1" 成对出现。

练习 ​

  1. 给底部卡片加上“无数据”占位:当 attrArr.length === 0 时展示空状态(自定义图片 + 文案),否则展示表格,二者严格二选一。
  2. 为表格的进出场各写一套过渡动画(进入时透明度 0→1、离开时缩放 1→0.6),用 name 与路由组件的过渡动画区分开,互不干扰。
  3. 把“添加属性值”按钮做成“未选三级分类时禁用”的效果,并说明为什么这里必须传布尔值而不是数字或字符串。
  4. 在点击“编辑”时把当前行的属性对象传给父组件,父组件据此把场景值切到添加/修改页;点击“取消”后切回表格,并检查是否残留了上一次编辑的数据。
  5. 手写一次新增属性的请求参数(属性名 + 一个属性值),然后在 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 等模块。