Skip to content

搭建SKU管理商品的静态与深度选择器(Day8 1/2) ​

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

学习目标 ​

  • 能够使用 Element Plus 的 el-card、el-table、el-table-column、el-pagination 独立搭建 SKU 管理模块静态页面,并正确设置序号列、固定操作列及分页器布局。
  • 能够封装 SKU 列表分页接口,在组件挂载和分页器页码、每页条数变化时发起请求,完成商品数据渲染、总条数展示与分页联动。
  • 能够根据 SKU 的上下架状态字段控制操作图标与颜色,调用上架、下架接口完成业务操作,并在操作成功后给出提示且保留当前分页状态刷新数据。
  • 能够使用 el-drawer、el-row、el-col、el-tag、el-carousel 搭建 SKU 商品详情静态结构,并说明栅格系统 24 栏布局及列间距 gutter 的使用方式。
  • 能够根据 SKU ID 请求商品详情接口,将商品名称、描述、价格、平台属性、销售属性和图片轮播等已有商品信息正确渲染到详情抽屉中。
  • 能够说明 scoped 生成 data-v-哈希值 并通过属性选择器实现局部样式的机制,并分别使用 >>>、/deep/、:deep() 穿透样式影响子组件非根节点元素。

知识点 ​

先明确 SKU 列表页的目标 ​

为什么需要:SKU 是库存里售卖商品的最小单位,后台必须能查看所有售卖商品,并控制其上下架。
怎么想:先把页面静态结构搭出来,再接接口渲染数据,最后补上下架业务。转写稿里明确:列表一挂载就要拿到所有售卖商品,共 122 条,并且分页展示。
怎么做:页面主体用 el-card 包 el-table,底部放 el-pagination;表格共 7 列:序号、名称、描述、默认图片、重量、价格、操作。
易错点:列宽加起来超过页面宽度时会出现横向滚动条,这是正常现象;操作列要固定在右侧,否则横向滚动时按钮会跑走。

表格静态与固定操作列 ​

为什么需要:操作列里有上架/下架、更新、查看详情、删除等按钮,必须始终可见。
怎么想:先按普通列写,发现横向滚动后操作列看不到,再给最后一列加 fixed="right"。转写稿里就是先看到“只能看到一部分”,再查 Element Plus 文档找到固定列写法。
怎么做:序号列用 type="index",名称列宽度约 180,操作列宽度约 280 并固定右侧。代码按转写稿整理如下:

vue
<el-table :data="skuArr" border>
  <el-table-column type="index" label="序号" width="80" align="center" />
  <el-table-column label="名称" width="180" />
  <el-table-column label="描述" />
  <el-table-column label="默认图片" />
  <el-table-column label="重量" />
  <el-table-column label="价格" />
  <el-table-column label="操作" width="280" fixed="right" />
</el-table>

易错点:fixed 不是随便加的,一般只给操作列加;列宽不要凭感觉反复试,先保证内容能看清,再处理横向滚动。

分页器静态与接口参数 ​

为什么需要:122 条商品不能一次性全铺开,必须分页请求。
怎么想:分页接口至少带两个参数:当前第几页、每页多少条。转写稿里反复强调“至少带两个参数”。
怎么做:定义 pageNo=ref(1)、pageSize=ref(10)、total=ref(0)、skuArr=ref([]);封装 getSkuList,组件挂载时调用一次;分页器绑定 current-page、page-size、total,并监听 current-change 和 size-change。
易错点:切换每页条数后要回到第一页;current-change 拿到当前页后要收集到 pageNo,再重新请求;接口返回一般是 result.records 和 result.total,不要直接拿整个对象当数组。

js
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const skuArr = ref([])

const getSkuList = async () => {
  const result = await reqSkuList(pageNo.value, pageSize.value)
  skuArr.value = result.records
  total.value = result.total
}

onMounted(() => {
  getSkuList()
})

const handleCurrentChange = (page) => {
  pageNo.value = page
  getSkuList()
}

const handleSizeChange = (size) => {
  pageSize.value = size
  pageNo.value = 1
  getSkuList()
}

易错点:size-change 里只改 pageSize 不够,还要重新请求;不要写成 pageNo.value = pageNo 这种没收集到参数的低级错误。

渲染 SKU 列表 ​

为什么需要:静态表格只是壳,真正要展示商品名称、描述、图片、重量、价格和售卖状态。
怎么想:普通字段直接用 prop,图片和操作按钮需要用插槽。
怎么做:表格绑定 :data="skuArr";名称、描述、重量、价格分别用 skuName、skuDesc、weight、price;图片用 el-image 插槽,地址取商品默认图片字段;操作列插槽里放按钮。
易错点:插槽里 row 代表当前商品;图片地址字段较长,转写稿里是现场查数据确认的,不能凭记忆写;图片过大时给 el-image 设置宽高。

上架与下架业务 ​

为什么需要:后台要控制前台商品是否售卖。转写稿明确:上架/下架对应前台商品能否展示。
怎么想:先看商品数据里的 isSale 字段。isSale === 1 表示当前在售,按钮应显示“下架”;否则显示“上架”。点击后根据状态发不同请求。
怎么做:上架、下架接口都要带商品 ID;请求成功后用 ElMessage.success 提示,并重新获取当前页数据,保持当前页。
易错点:isSale === 1 时点击是下架,不是上架;刷新时不要固定回第一页,否则用户在第 3 页操作后会跳走;这两个接口按转写稿说没有返回值,类型可定义为 void。

js
const handleSale = async (row) => {
  if (row.isSale === 1) {
    await reqSkuCancelSale(row.id)
    ElMessage.success('下架成功')
  } else {
    await reqSkuSale(row.id)
    ElMessage.success('上架成功')
  }
  getSkuList()
}

易错点:参数是商品 ID,不是分类 ID;按钮图标要根据 isSale 切换上下箭头,颜色可自己选,但判断条件不能反。

详情抽屉与样式穿透补充 ​

为什么需要:查看商品详情要展示名称、描述、价格、平台属性、销售属性、图片等,普通 dialog 在中间弹出,长内容不方便,转写稿改用 el-drawer。
怎么想:抽屉用 v-model 控制显示,内部用 el-row/el-col 栅格布局,图片用 el-carousel 轮播。
怎么做:点击“查看”时先打开抽屉,再根据商品 ID 请求详情;栅格用 :span="8" 和 :span="16" 分左右。
易错点:组件加了 scoped 后,父组件样式默认影响不到子组件内部;需要样式穿透时用 :deep(选择器)。转写稿强调:scoped 是给当前组件元素和子组件根节点加哈希属性,并不是“完全不能影响子组件”。

2.1 scoped 要解决什么问题 ​

为什么需要:组件化开发里,每个组件都写样式,如果样式默认全局生效,很容易互相污染。比如父组件写了一个 h1 { color: red; },子组件里的 h1 也可能被染红。我们希望样式“只影响当前组件”,于是 Vue 提供了 <style scoped>。

怎么想:先别急着记结论,做一个最小实验:在 App 里引入一个 Test 子组件,父子结构里都放 h1。先不加 scoped,刷新页面,两个 h1 都会被样式匹配;再给父组件样式加 scoped,观察 DOM 和样式匹配结果。

怎么做:

vue
<!-- App.vue -->
<template>
  <div>
    <h1>我是父组件</h1>
    <Test />
  </div>
</template>

<script setup>
import Test from './components/Test.vue'
</script>

<style scoped>
h1 {
  color: red;
}
</style>
vue
<!-- Test.vue -->
<template>
  <div>
    <h1>我是子组件</h1>
  </div>
</template>

加 scoped 后,审查元素会看到当前组件 DOM 元素、子组件根节点上出现类似 data-v-xxxx 的自定义属性。样式最终不是简单的 h1,而是类似:

css
h1[data-v-xxxx] {
  color: red;
}

易错点:不要只说“scoped 是局部样式”就结束。它的底层是:scoped 生成 data-v-hash,再通过属性选择器提高匹配条件,从而实现局部匹配。

scoped 的底层推导:data-v-hash + 属性选择器 ​

为什么需要理解底层:只背“局部样式”会遇到反例:为什么父组件的 scoped 有时还能影响子组件?为什么子组件非根节点又影响不到?必须看 hash 加在哪。

怎么想:转写稿里的调试过程是:父组件加 scoped 后,父组件 DOM 节点和子组件根节点都会带上相同的 data-v-hash;而子组件内部非根节点不会带父组件的这个 hash。于是 h1[data-v-父hash] 只能匹配父组件的 h1 和“恰好作为子组件根节点”的元素。

怎么做:把父组件样式写成 h1[data-v-xxxx] 来理解。若子组件根节点是 div,父组件的 h1 选择器匹配不到子组件内部的 h1;若子组件根节点本身就是 h1,它带父 hash,就可能被父样式命中。

易错点:说“scoped 只影响当前组件”是半句话。更准确:scoped 会让当前组件 DOM 元素以及子组件根节点带相同 hash,所以父样式可能影响子组件根节点;但子组件内部非根节点默认影响不到。

父子都 scoped 时,hash 为什么不一样 ​

为什么需要:有同学会问:父组件加 scoped,子组件也加 scoped,那父样式能不能穿透到子组件内部?答案通常是不能。

怎么想:父组件生成父 hash,子组件生成子 hash。子组件根节点可能同时带父 hash 和子 hash,但子组件内部元素只带子 hash。父组件的 h1[data-v-父hash] 匹配不到只带子 hash 的内部 h1。

怎么做:在父子组件都加 scoped 后审查元素,对比 data-v- 值;再看编译后的选择器条件。这样就能解释“为什么父样式进不去子组件内部”。

易错点:不要靠删除 scoped 改成全局样式来解决穿透,这会污染其他组件;需要穿透时用深度选择器。

深度选择器:需要穿透时怎么做 ​

为什么需要:实际开发中常要改子组件内部样式,比如轮播图指示器、表格某列等。父组件加了 scoped 又影响不到子组件内部,就需要“样式穿透”。

怎么想:深度选择器的本质是让编译后的选择器不再被当前 scoped 的 hash 条件卡住,从而匹配子组件内部元素。转写稿提到三种常见写法:原生 CSS 的 >>>;Less/Sass 等预处理器中的 ::v-deep;新版本 Vue 更推荐的 :deep(选择器)。

怎么做:

css
/* 原生 CSS 中可用 */
>>> h1 {
  color: red;
}

/* Less/Sass 等预处理器中常见 */
::v-deep h1 {
  color: red;
}

/* Vue 3 推荐写法 */
:deep(h1) {
  color: red;
}

易错点:>>> 在预处理器里可能失效;:deep() 要把选择器写进括号;深度选择器不要滥用,否则会破坏组件封装,优先考虑组件自身提供的 props、插槽或样式变量。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:scoped 局部样式的底层实现 + 三种深度选择器 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>scoped 与深度选择器</title>
  <style>
    /* ===== 下面是父组件 <style scoped> 编译后的「真实产物」 ===== */

    /* ① 源码写 h1 { color: red }  →  编译成 h1[data-v-90] */
    h1[data-v-90] { color: red; }

    /* ② 源码写 .app :deep(.child h1) { color: green }
          →  编译成 .app[data-v-90] .child h1  (hash 只加在 :deep 前面那截) */
    .app[data-v-90] .child h1 { color: green; }

    /* ③ 不加 scoped 的就是全局样式:h2 { color: #999 } */
    h2 { color: #999; }

    body { font-family: system-ui, sans-serif; padding: 20px; }
  </style>
</head>
<body>
  <!-- 父组件模板:<div class="app"><h1>父</h1><Child /></div> -->
  <div class="app" data-v-90>
    <h1>父组件的 H1 —— 红色</h1>

    <!-- 子组件根节点:同时带自己的 data-v-91 和父传下来的 data-v-90 -->
    <div class="child" data-v-90 data-v-91>
      <h1>子组件的 H1 —— 绿色(靠 :deep 穿透)</h1>
      <h2>子组件里的 H2 —— 灰色(全局样式生效)</h2>
    </div>
  </div>

  <script>
    // 直观验证:哪些元素带上了 data-v-90 这个自定义属性
    document.querySelectorAll('[data-v-90]').forEach((el) => {
      console.log(el.tagName, '| class:', el.className, '| dataset:', el.dataset);
    });
  </script>
</body>
</html>

关键行与易错点

  • h1[data-v-90] 就是 scoped 的真相:scoped 会给当前组件的 DOM 元素打上 data-v-哈希 自定义属性,样式选择器末尾再拼一个属性选择器,于是只有带这个哈希的元素才匹配得上,这就是“局部样式”。
  • 子组件的根节点会同时拥有两个哈希(自己的 data-v-91 和父组件传下来的 data-v-90),但子组件内部的 h1 只有 data-v-91。所以笔记里说“scoped 是局部样式”这句话只对一半——父组件的普通 scoped 样式会命中子组件的根节点。
  • 想穿透到子组件内部(比如 el-carousel 的指示器),必须用深度选择器。三种写法:原生 CSS / 预处理器用 >>>,less 老版本用 ::v-deep,Vue3 推荐用 :deep(选择器)。
  • 易错点::deep() 括号里写的才是“穿透”的部分,外面那截仍然会被加上 hash;写成 :deep(.child h1) 编译后是 .app[data-v-90] .child h1,写成 .child :deep(h1) 编译结果是 .child[data-v-90] h1,作用范围完全不同。

示例二:SKU 管理表格静态搭建 + 分页器 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>SKU 管理 —— 静态搭建与分页</title>
  <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
  <style>
    /* 表格与分页器之间留点空隙 */
    .el-table { margin: 10px 0; }
  </style>
</head>
<body>
<div id="app">
  <el-card>
    <el-table :data="skuArr" border>
      <el-table-column type="index" label="序号" width="80" align="center" />
      <el-table-column label="名称" width="180" prop="skuName" />
      <el-table-column label="描述" prop="skuDesc" />
      <el-table-column label="默认图片" width="180">
        <template #default="{ row }">
          <img :src="row.skuDefaultImg" style="width: 100px; height: 100px" />
        </template>
      </el-table-column>
      <el-table-column label="重量" prop="weight" />
      <el-table-column label="价格" prop="price" />
      <!-- 最后一列固定到右侧 -->
      <el-table-column label="操作" width="280" fixed="right" />
    </el-table>

    <el-pagination
      v-model:current-page="pageNo"
      v-model:page-size="pageSize"
      :total="total"
      :page-sizes="[10, 20, 30, 40]"
      layout="prev, pager, next, jumper, ->, total, sizes"
      background
      @current-change="getSkuList"
      @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/dist/index.full.js"></script>
<script>
const { createApp, ref, onMounted } = Vue;

/* ===== 接口层:真实项目里换成 axios 二次封装 =====
   GET /api/product/sku/{page}/{limit}
   返回 { records, total, size, current, pages }        */
const REQ_SKU_LIST = async (page, limit) => {
  await new Promise((r) => setTimeout(r, 100));
  const all = Array.from({ length: 122 }, (_, i) => ({
    id: i + 1,
    skuName: '商品名称' + (i + 1),
    skuDesc: '商品描述' + (i + 1),
    skuDefaultImg: 'https://via.placeholder.com/100',
    weight: 100 + i,
    price: 12.5 + i,
    isSale: i % 3 === 0 ? 1 : 0
  }));
  return {
    records: all.slice((page - 1) * limit, page * limit),
    total: all.length,
    size: limit,
    current: page,
    pages: Math.ceil(all.length / limit)
  };
};

const app = createApp({
  setup() {
    const skuArr = ref([]);   // 存储全部商品(当前页)
    const total = ref(0);     // 商品总个数
    const pageNo = ref(1);    // 当前第几页
    const pageSize = ref(10); // 每页几条

    // 挂载时发一次,翻页时再发 —— 所以必须封装成函数
    const getSkuList = async () => {
      const res = await REQ_SKU_LIST(pageNo.value, pageSize.value);
      skuArr.value = res.records;
      total.value = res.total;
    };

    // 每页条数变化:先回到第一页,再重新拉
    const handleSizeChange = () => {
      pageNo.value = 1;
      getSkuList();
    };

    onMounted(() => { getSkuList(); });

    return { skuArr, total, pageNo, pageSize, getSkuList, handleSizeChange };
  }
});
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>

关键行与易错点

  • type="index" 生成序号列,不用自己写 scope.$index + 1。
  • “默认图片”列必须用 #default="{ row }" 插槽,因为要渲染 <img> 标签而不是纯文本。
  • 7 列宽度加起来超过容器宽度 → 出现横向滚动条是正常现象,正因如此才需要 fixed="right" 把操作列钉在右侧(fixed="left" 也可以)。
  • 分页器 layout 里的 -> 表示“把后面的项推到最右边”;background 给页码加背景色。
  • 易错点 1:pageNo、pageSize 是 ref,发请求时必须写 .value,直接写 pageNo 会传一个对象给接口。
  • 易错点 2:@size-change 里一定要先把 pageNo 重置为 1,否则从第 12 页切到“每页 40 条”可能停在空页。
  • 易错点 3:分页接口至少带两个参数——当前第几页 + 每页多少条,这是通用套路。

示例三:商品的上架与下架业务 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>SKU 上架与下架</title>
  <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
</head>
<body>
<div id="app">
  <el-card>
    <el-table :data="skuArr" border>
      <el-table-column type="index" label="序号" width="80" align="center" />
      <el-table-column label="名称" width="180" prop="skuName" />
      <el-table-column label="描述" prop="skuDesc" />
      <el-table-column label="默认图片" width="180">
        <template #default="{ row }">
          <img :src="row.skuDefaultImg" style="width: 100px; height: 100px" />
        </template>
      </el-table-column>
      <el-table-column label="重量" prop="weight" />
      <el-table-column label="价格" prop="price" />
      <el-table-column label="操作" width="280" fixed="right">
        <template #default="{ row }">
          <!-- isSale === 1 说明在售 → 显示「下架」图标;已下架 → 显示「上架」图标 -->
          <el-button
            size="small"
            :type="row.isSale === 1 ? 'warning' : 'success'"
            :icon="row.isSale === 1 ? 'Bottom' : 'Top'"
            @click="updateSale(row)"
          />
          <el-button size="small" type="primary" icon="Edit" @click="updateSku" />
          <el-button size="small" type="info" icon="InfoFilled" @click="

常见错误 ​

1. 上下架图标不随商品状态变化

  • 现象:列表里所有行的操作图标、颜色一模一样,明明下架的商品也显示“下架”按钮。
  • 原因:模板里把图标名和 type 写死了,没有按 row.isSale 做三元判断,只把接口请求写对了。
  • 修法:图标与颜色都写成 row.isSale ? 'Bottom' : 'Top' 这类三元表达式,颜色同理(在售用 warning、下架用 info),改完拿一条 isSale=1 和一条 isSale=0 的数据各点一次验证。

2. 上下架成功后列表“跳回第一页”

  • 现象:在第 3 页点上下架,刷新后表格回到第 1 页,用户得重新翻。
  • 原因:刷新逻辑里把页码参数写死成了 pageNo: 1(直接从初始值复制过来)。
  • 修法:刷新时复用当前的 pageNo / pageSize,只在“切换每页条数”时才是 pageNo = 1。

3. 分页器切条数后数据对不上

  • 现象:一页 10 条切到 20 条,页码显示回到第 1 页,但表格内容还是旧的 10 条,或者请求参数与 UI 不一致。
  • 原因:回调里只改了 pageSize 的 ref,或者只改了 pageNo 却没重新发请求,参数收集与请求时机脱节。
  • 修法:size-change 回调里三件事按顺序做——同步 pageSize、把 pageNo 归 1、再调一次列表请求函数;current-change 只同步页码再请求。列表请求一定抽成独立函数给两处复用,别在 onMounted 里内联写。

4. scoped 下改不动组件内部结构(抽屉、轮播图指示器)

  • 现象:给 el-carousel 的指示器、el-drawer 内部元素写样式,怎么刷新都没效果。
  • 原因:scoped 编译后给选择器加了属性选择器,只能命中本组件 DOM 与子组件根节点,进不到子组件内部深层节点。
  • 修法:用深度选择器穿透,Vue3 推荐 :deep(选择器) { };不要图省事把 <style scoped> 直接改成全局样式,会污染其他页面。

5. 深度选择器写法过时导致不生效

  • 现象:抄旧笔记写 ::v-deep .xxx 或 /deep/ .xxx,控制台警告、样式不生效。
  • 原因:/deep/、::v-deep 是旧版编译器写法,新版 Vue3 编译链要求函数式 :deep(...)。
  • 修法:统一改成 :deep(.el-carousel__indicator) { } 这种形式,选择器写在括号内。

6. 查看详情传错 ID

  • 现象:抽屉能弹出,但名称、价格全空。
  • 原因:把 categoryId / spuId 当成商品 ID 传给了详情接口,或者传了整个 row 对象。
  • 修法:只传 row.id(当前 SKU 自身的主键),发请求前 console.log 一次入参,对照接口文档确认参数名。

7. 轮播图图片出不来

  • 现象:抽屉里轮播区域是空白或只有 alt 文字。
  • 原因:图片字段路径写错(不是 row.image 而是 skuInfo.skuDefaultImg 之类),或 el-carousel-item 里 img 没设宽高。
  • 修法:先 console.log 拿到真实字段路径再绑定 src;给 img 补 width:100%; height:100%,并确认轮播容器有明确高度。

8. 栅格布局用错单位

  • 现象:抽屉里左右两栏宽度不对,或一行内容被挤成上下两行。
  • 原因:不熟 24 栅格,用固定像素宽度硬撑,或者 el-col 忘了写 span。
  • 修法:一行的份额永远是 24,左 8 右 16 之类按 span 分配;列间距用 el-row 的 gutter,不要给 el-col 手写 margin。

练习 ​

  1. 把 SKU 列表请求抽成 getSkuList(),在 onMounted、current-change、size-change 三处复用,并保证:翻页时保留当前每页条数,切条数时页码归 1。写完在页面上验证“第 3 页 → 切 20 条 → 再翻到第 2 页”参数是否正确。

  2. 实现上下架:点击按钮时按 row.isSale 决定调上架还是下架接口,成功后给 ElMessage 提示并刷新当前页数据。要求在第 2 页操作后仍停留在第 2 页。

  3. 给表格操作列加 fixed="right",把列宽合计调到超出容器宽度,验证横向滚动时操作列固定在右侧、且不遮挡相邻列内容。

  4. 用 el-drawer + el-row/el-col 搭出商品详情静态:名称、描述、价格、平台属性(el-tag 循环)、销售属性(el-tag 循环)、商品图片(el-carousel 循环)。要求左右两栏用 span 分配、用 gutter 控间距、每块之间用 el-divider 分隔。

  5. 用 :deep() 把轮播图指示器改成圆点样式(参考默认线性指示器的 DOM 结构),并在笔记里写清:为什么这里必须用深度选择器,以及 scoped 为什么单独写 .el-carousel__indicator 会失效。

原视频对照 ​

#视频时长
1SKU模块静态的搭建13:47
2SKU管理模块数据的渲染与分页的完成20:59
3完成商品的上架与下架的业务16:48
4完成展示SKU商品的静态的搭建--栅格系统19:27
5渲染已有的商品信息15:32
6三种深度选择器使用(scoped)20:57

小结 ​

  1. 分页类接口的请求参数永远是“当前页 + 每页条数”,UI 上的页码/条数变化必须同步到参数并触发一次新请求,三者缺一不可。
  2. 列表请求一定要封装成函数:挂载、翻页、切条数、增删改后的刷新,全都是调同一个函数,写死参数是 bug 的高发点。
  3. 状态类按钮(上架/下架、启用/禁用)的图标、颜色、点击后调的接口,三者必须由同一个字段驱动,否则界面和业务必然脱节。
  4. scoped 的“局部”是半真半假:它能管住本组件 DOM 和子组件根节点,管不住子组件内部深层结构。
  5. 深度选择器是穿透 scoped 的唯一正路,Vue3 统一用 :deep(选择器),旧写法别再用;能用组件自身 API(如 props)解决的样式问题,优先不用深度选择器。
  6. 布局优先用 24 栅格(el-row / el-col + span + gutter),别用固定像素硬拼,屏幕变化时栅格能自动适配。
  7. 详情类功能的关键是“拿对 ID、拿对字段路径”:先把接口返回值打印出来看清结构,再写模板绑定,比反复猜字段名快得多。
  8. 后台模块的套路高度同质化:静态骨架 → 请求封装 → 数据渲染 → 分页 → 行内操作。把这一套练成肌肉记忆,新模块的边际成本会非常低。