Skip to content

SPU与SKU理解(Day5 2/2) ​

本页对应 day5,共 6 段视频,约 1 小时 36 分。

学习目标 ​

  • 能够使用 lodash 的 cloneDeep 对引用类型数据进行深拷贝,并说明深拷贝与浅拷贝在属性更新场景中的差异。
  • 能够根据属性 ID 完成删除已有属性业务,包括确认弹窗、调用删除 API、成功提示及删除后重新获取属性列表。
  • 能够结合电商场景解释 SPU 与 SKU 的含义,并举例区分标准化产品单元与库存量最小单位。
  • 能够使用三级分类组件、卡片、表格、分页器等完成 SPU 管理模块静态页面搭建。
  • 能够封装获取 SPU 列表 API,定义相关 TS 类型,并在三级分类变化时请求并渲染已有 SPU 数据。
  • 能够处理分页器页码和每页条数变化事件,重新请求并更新 SPU 列表数据。

知识点 ​

一、SPU与SKU:先建立业务概念 ​

为什么需要:SPU、SKU是电商后台和面试里绕不开的术语。转写稿里明确提醒:简历写了SPU管理模块,面试官问“什么是SPU、什么是SKU”却答不上来,会很被动。
怎么想:不要把二者当成两个普通字段。SPU描述“这是哪款产品”,SKU描述“具体卖的是哪一个最小库存单元”。SPU是标准化产品单元,SKU是库存量最小单位。
怎么做:先记定义,再用商品举例。比如“苹果6手机”是SPU;颜色、内存、系统、价格等销售属性组合后,形成具体SKU,如“红色、256、苹果、6999”和“蓝色、128、塞班、1299”。转写稿还强调:销售属性最多三个,常见是颜色、版本、尺码。
易错点:不要把SPU说成“类”、SKU说成“实例”就结束,面试表达要回到电商语言;SPU不是具体库存,SKU才是售卖和库存的最小单位;属性组合要能唯一标识一个售卖单元,不能把不相关参数硬凑。

二、用苹果6手机把概念落到例子上 ​

为什么需要:只背定义容易混。转写稿用“苹果6手机”反复推导,就是为了把“产品”与“具体售卖规格”分开。
怎么想:先问“这是哪家公司的哪款产品”,答案是SPU;再问“这款产品下到底卖哪一版”,答案是SKU。颜色红、紫、蓝,内存128、256,系统安卓、苹果、塞班,价格1299、6999、899,都是可选的销售属性值。
怎么做:把SPU写成“苹果6手机”,把SKU写成属性组合。例如SKU:红色、256、苹果、6999;SKU:蓝色、128、塞班、1299。后续SPU管理模块里,新增SPU要填品牌、名称、描述、图片、销售属性;新增SKU要选具体销售属性值和平台属性。
易错点:价格、系统是否作为销售属性要看业务设计,转写稿示例只是帮助理解;不要把所有参数都塞进SKU名称;SKU必须能对应真实可售、可库存的商品。

三、2.1 SPU静态搭建:先搭骨架 ​

为什么需要:SPU模块数据量大、接口多。转写稿明确说这个模块难,所以先把静态结构搭好,再接数据,避免“结构、类型、请求、渲染”混在一起。
怎么想:静态页先复用三级分类全局组件,再放卡片、添加SPU按钮、表格和分页器。转写稿说“先把静态搭了,底部数据后面再分场景”。
怎么做:页面顶部引入三级分类组件,静态阶段传场景值并处理禁用/启用;卡片左上角放“添加SPU”按钮,使用primary类型和plus图标;表格先做四列:序号、SPU名称、SPU描述、SPU操作;操作列预留添加SKU、更新SPU、查看SKU、删除SPU等按钮;底部放分页器,配置当前页码和每页条数。
易错点:三级分类组件不传场景值或禁用状态,容易提前可操作;描述列过长要处理列宽和内容隐藏;分页器的“页码变化”和“每页条数变化”不是同一个事件,不能混用。

四、静态搭建后的调试与后续接数据注意 ​

为什么需要:转写稿里反复出现“先刷新看骨架、再测接口、再看渲染”的过程,说明静态搭建不是写完就算,必须验证。
怎么想:先确认三级分类是否按预期禁用,按钮图标和文字是否正常,表格列是否齐全,分页器是否显示。后续接接口时,请求至少要带当前页、每页条数、三级分类ID。
怎么做:转写稿中的调试顺序是:先测接口能否拿到数据,再监听三级分类ID,有值才发请求;每次分类变化要清空上一次的SPU数组;页码变化用current-change收集当前页码,每页条数变化用size-change收集条数,再重新请求。
易错点:三级分类ID没有值时不要发请求;切换分类不清空旧数据会串数据;把页码事件和条数事件合并成一个回调,参数含义会混;没有数据时不要只显示空表格,可用空状态或分割线提示。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码一:SPU 与 SKU 的关系(纯 HTML + JS,直接双击运行)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SPU 与 SKU</title>
<style>
  body { font-family: "Microsoft YaHei", sans-serif; padding: 20px; background: #f0f2f5; }
  .spu { border: 1px solid #dcdfe6; border-radius: 6px; padding: 12px 16px; width: 560px; background: #fff; }
  .spu h3 { margin: 0 0 8px; color: #303133; }
  .spu p { margin: 6px 0; color: #606266; font-size: 14px; }
  .sku-item { padding: 8px 12px; border-left: 3px solid #409eff; background: #f5f7fa; margin-bottom: 6px; font-size: 14px; }
</style>
</head>
<body>
<div id="app"></div>

<script>
  // ===== 1. SPU:标准化产品单元(相当于“类”) =====
  const spu = {
    name: '苹果6手机',
    // 销售属性(电商里最多 3 个:颜色、版本、尺码)
    saleAttrs: {
      color:  ['红色', '紫色', '蓝色'],
      memory: [128, 256],
      system: ['安卓', '苹果', '塞班']
    },
    price: [1299, 6999, 899]
  };

  // ===== 2. SKU:库存量最小单位(相当于“类的实例”,即真正售卖的那一件) =====
  const skus = [
    { color: '红色', memory: 256, system: '苹果', price: 6999 },
    { color: '蓝色', memory: 128, system: '塞班', price: 1299 }
  ];

  // ===== 3. 用 SPU 的销售属性做笛卡尔积,推出它一共能拆出多少个 SKU =====
  function buildSkus(spu) {
    const keys = Object.keys(spu.saleAttrs);
    return keys.reduce((acc, key) => {
      const next = [];
      acc.forEach(item => {
        spu.saleAttrs[key].forEach(val => next.push({ ...item, [key]: val }));
      });
      return next;
    }, [{}]);
  }

  const all = buildSkus(spu);

  document.getElementById('app').innerHTML = `
    <div class="spu">
      <h3>SPU:${spu.name}</h3>
      <p>颜色:${spu.saleAttrs.color.join('、')};
         内存:${spu.saleAttrs.memory.join('、')}G;
         系统:${spu.saleAttrs.system.join('、')}</p>
      <p>该 SPU 按销售属性可拆出 <b>${all.length}</b> 个 SKU,其中已上架 ${skus.length} 个:</p>
      ${skus.map(s => `<div class="sku-item">
          SKU:${s.color} / ${s.memory}G / ${s.system} / ¥${s.price}
        </div>`).join('')}
    </div>
  `;
</script>
</body>
</html>

要点解释:

  • spu 对象里只放“这个产品有哪些可选项”,它本身不是一件能下单的商品;skus 才是能加购物车、能扣库存的最小单位。
  • buildSkus 用 reduce + 展开运算符做笛卡尔积:3 色 × 2 内存 × 3 系统 = 18 个 SKU,这正是后台“添加 SKU”页面要一条条选出来的东西。
  • 易错点:面试被问“SPU 是什么”时,别答成“类”,要答“标准化产品单元”;SKU 答“库存量最小单位 / 售出的最小单位”。
  • 易错点:销售属性最多 3 个(颜色、版本、尺码),不是想加几个加几个。

代码二:SPU 管理模块静态搭建(Vue3 + Element Plus,CDN 直接运行)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SPU 管理 - 静态搭建</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<style>
  body { margin: 0; padding: 20px; background: #f0f2f5; }
  .card-gap { margin-top: 20px; }
  .table-gap { margin-top: 10px; }
</style>
</head>
<body>
<div id="app">
  <!-- 上半部分:三级分类全局组件(真实项目换成 Category 组件,场景值 scene 禁用切换) -->
  <el-card>
    <el-select disabled placeholder="请选择三级分类" style="width: 240px;">
      <el-option label="手机通讯 / 手机" value="61" />
    </el-select>
  </el-card>

  <!-- 下半部分:SPU 列表卡片 -->
  <el-card class="card-gap">
    <el-button type="primary" icon="Plus">添加SPU</el-button>

    <el-table border class="table-gap" :data="spuArr">
      <el-table-column type="index" label="序号" width="80" align="center" />
      <el-table-column label="SPU名称" prop="spuName" />
      <!-- 描述过长时省略号 + 悬浮提示,避免行高被撑得参差不齐 -->
      <el-table-column label="SPU描述" prop="description" show-overflow-tooltip />
      <el-table-column label="SPU操作" width="280">
        <template #default="{ row }">
          <el-button type="primary" size="small" icon="Plus"      title="添加SKU" />
          <el-button type="warning" size="small" icon="Edit"      title="更新SPU" />
          <el-button type="info"    size="small" icon="InfoFilled" title="查看SKU列表" />
          <el-button type="danger"  size="small" icon="Delete"    title="删除SPU" />
        </template>
      </el-table-column>
    </el-table>

    <!-- 空状态:有数据展示表格,没数据显示空状态 -->
    <el-empty v-if="!spuArr.length" description="暂无数据" />

    <el-pagination
      style="margin-top: 20px;"
      :current-page="pageNo"
      :page-size="pageSize"
      :page-sizes="[3, 5, 7, 9]"
      layout="prev, pager, next, jumper, ->, sizes, total"
      :total="total" />
  </el-card>
</div>

<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>
<script>
  const { createApp, ref } = Vue;

  const app = createApp({
    setup() {
      // 静态搭建阶段先用假数据把表格撑起来
      const spuArr = ref([
        { id: 1, spuName: '苹果12',  description: '苹果12,5G手机,性能不错' },
        { id: 2, spuName: '红米10X', description: '红米10X 性价比高' },
        { id: 3, spuName: '小米10',  description: '小米10 旗舰机型' }
      ]);
      const pageNo = ref(1);    // 当前页码,默认第一页
      const pageSize = ref(3);  // 每页展示条数,默认 3 条
      const total = ref(7);     // 总条数
      return { spuArr, pageNo, pageSize, total };
    }
  });

  // 全局注册 Element Plus 与全部图标,模板里才能直接写 icon="Plus"
  app.use(ElementPlus);
  for (const [key, comp] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, comp);
  }
  app.mount('#app');
</script>
</body>
</html>

要点解释:

  • 三级分类组件上先写 disabled,是因为此时还没选分类、也没拿到 c3Id;真实项目里是 :disabled="!c3Id"。
  • show-overflow-tooltip 加在“SPU描述”这一列上,内容过长才省略并悬浮显示,不加的话表格行高会被撑得参差不齐。
  • layout="prev, pager, next, jumper, ->, sizes, total" 里的 -> 是弹簧,把 sizes 和 total 推到最右边,这是分页器好看的常用写法。
  • 易错点:el-table-column 想禁止拖拽调宽,得在列上找属性(resizable),el-table 本身没有这个开关。
  • 易错点:type="index" 每页都从 1 开始;要跨页连续编号得写 (pageNo - 1) * pageSize + index + 1。

代码三:监听三级分类 + 获取 SPU 列表 + 分页联动(Vue3 + Element Plus,CDN 直接运行)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SPU 列表数据获取与分页联动</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<style> body { margin: 0; padding: 20px; background: #f0f2f5; } </style>
</head>
<body>
<div id="app">
  <!-- 模拟三级分类全局组件:选中后仓库里的 c3Id 才有值 -->
  <el-card>
    <el-select v-model="c3Id" placeholder="请选择三级分类" style="width: 260px;">
      <el-option label="手机通讯 / 手机" value="61" />
      <el-option label="图书 / 电子书刊" value="62" />
    </el-select>
  </el-card>

  <el-card style="margin-top: 20px;">
    <el-table v-if="spuArr.length" border :data="spuArr">
      <el-table-column type="index" label="序号" width="80" align="center" />
      <el-table-column label="SPU名称" prop="spuName" />
      <el-table-column label="SPU描述" prop="description" show-overflow-tooltip />
    </el-table>
    <el-empty v-else description="暂无数据" />

    <el-pagination
      style="margin-top: 20px;"
      :current-page="pageNo"
      :page-size="pageSize"
      :page-sizes="[3, 5, 7, 9]"
      layout="prev, pager, next, jumper, ->, sizes, total"
      :total="total"
      @current-change="handleCurrentChange"
      @size-change="handleSizeChange" />
  </el-card>
</div>

<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>
<script>
  const { createApp, ref, watch } = Vue;

  // ===== 模拟后端数据 =====
  const mockDB = {
    '61': [
      { id: 1, spuName: '苹果12',   description: '苹果12,5G手机' },
      { id: 2, spuName: '红米10X',  description: '红米10X 性价比高' },
      { id: 3, spuName: '小米10',   description: '小米10 旗舰机型' },
      { id: 4, spuName: '华为Mate20', description: '华为Mate20 5G手机' },
      { id: 5, spuName: 'OPPO 20',  description: 'OPPO 20 Plus' },
      { id: 6, spuName: '苹果11',   description: '苹果11' },
      { id: 7, spuName: '荣耀30',   description: '荣耀30' }

常见错误 ​

  1. 编辑属性后取消,原列表数据也跟着变 → 原因:直接把已有属性对象赋给收集对象,引用类型仍指向同一数组 → 修法:对来源数据使用 cloneDeep 深拷贝后再赋值。
  2. 点击删除属性“确定”没反应或没刷新列表 → 原因:Popconfirm 未绑定 @confirm,或删除成功后未重新请求属性列表 → 修法:在 confirm 中按属性 ID 调删除接口,成功后提示并重新 getAttr。
  3. 删除失败时出现重复提示 → 原因:业务层又写了一套失败提示,响应拦截器已统一处理 → 修法:业务层只处理成功分支,失败提示交给拦截器。
  4. SPU 页面未选三级分类就发请求,或切换分类后仍显示旧数据 → 原因:watch 中未判断三级分类 ID,且未清空上一次列表 → 修法:有 C3ID 才请求,变化时先清空 SPUArr,默认页码传 1。
  5. 分页页码和每页条数共用一个回调,切换条数后请求参数错乱 → 原因:current-change 与 size-change 注入参数含义不同 → 修法:分开处理;size-change 中先重置当前页为 1,再请求列表。
  6. SPU 表格描述列高度参差、列宽被随意拖拽、无数据时页面空白 → 原因:未配置列溢出提示、列可拖拽属性、空状态分支 → 修法:描述列加 show-overflow-tooltip,目标列设 :resizable="false",列表长度用 v-if 切换表格与空状态。
  7. 系统内置属性被误删/误改 → 原因:未区分系统数据与自定义数据 → 修法:系统数据按只读处理,删除/编辑入口禁用或拦截。

练习 ​

  1. 在“更新已有属性”场景中,指出哪一处赋值会引发引用共享问题,并用 lodash 的 cloneDeep 给出改造方案。
  2. 完成“删除已有属性”链路:Popconfirm 确认事件、删除 API、成功提示、成功后刷新列表,并说明失败提示应放在哪里。
  3. 为 SPU 列表编写三级分类监听的正确流程:未选分类、切换分类、清空旧数据、默认页码、发起请求,列出关键判断。
  4. 分页器接入练习:分别处理 current-change 和 size-change,说明为什么不能合并;写出 size-change 中重置页码并重新请求的顺序。
  5. 排查 SPU 静态页体验问题:添加按钮未禁用、描述列高度不齐、列可拖拽、无数据空白,分别给出对应配置或指令。

原视频对照 ​

#视频时长
1深拷贝使用lodash06:31
2完成删除已有的属性的业务08:35
3SPU模块业务的介绍20:21
4完成SPU模块静态的搭建11:11
5书写已有的SPU的API与数据的展示38:03
6渲染已有的SPU的数据11:51

小结 ​

  1. 属性更新模块最重要的风险点是引用类型共享,深拷贝必须掌握并能说清原因。
  2. 删除业务要形成闭环:确认、请求、成功提示、刷新列表;失败提示优先复用拦截器。
  3. SPU/SKU 要能用业务语言解释清楚:SPU 是标准化产品单元,SKU 是库存量最小单位。
  4. SPU 模块难点在数据量大、接口多、返回数据常不完整,应先看接口实际返回,再补 TS 类型。
  5. 三级分类 ID 是 SPU 列表请求的前置条件:未选不发请求,切换要清旧数据,按钮状态要联动。
  6. 分页两个事件参数语义不同,不能图省事合并;每页条数变化时应回到第一页。
  7. 表格体验细节要纳入验收:列宽拖拽、内容溢出、空状态、分割线都属于可交付质量。
  8. 系统数据只读、自定义数据可维护,这是属性管理模块必须守住的数据边界。