Skip to content

完成新增属性与属性值业务与SPU与SKU理解(Day5 1/2) ​

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

学习目标 ​

  1. 能收集并组装新增属性请求参数(categoryId、categoryLevel、attrName、attrValueList),调用新增/更新共用接口完成新增,并在成功后提示、切回场景 0、重新拉取属性列表。
  2. 能通过属性值行对象上的布尔标记控制 input 与 div 的显示切换,实现编辑模式与查看模式的逐行切换且各行互不影响。
  3. 能在失焦切换查看模式前校验属性值不能为空、不能为纯空格、不能重复,并能排除当前行自身后判断重复;非法时给出提示并阻止切换或删除对应空值行。
  4. 能在新增属性值及点击 div 进入编辑模式时,通过 nextTick 获取输入框实例并调用聚焦方法,实现切换后自动聚焦。
  5. 能完成属性值添加、删除及保存按钮禁用逻辑,确保属性名和至少一个属性值存在后才允许提交。
  6. 能点击已有属性进入修改场景,将当前行数据合并到表单并展示,使用深拷贝避免修改时联动表格数据,同时清理 id 后调用接口完成更新并提示结果。

知识点 ​

新增属性与属性值的参数结构 ​

为什么需要:新增属性不是只提交一个名字,后端还要知道这个属性属于哪个三级分类、是几级分类、包含哪些属性值。参数结构必须先对齐接口,否则请求发出去也会失败。

怎么想:把接口需要的字段拆成“分类信息 + 属性名 + 属性值数组”。其中 categoryId 是当前三级分类 ID,categoryLevel 在给三级分类新增时固定为 3,attrName 来自属性名称输入框,attrValueList 是属性值数组,每一项用 valueName 收集用户输入。

怎么做:本节笔记给出的携带参数如下,写代码时按这个结构组织 attrParams:

json
{
  "categoryId": 61,
  "categoryLevel": 3,
  "attrName": "豪哥",
  "attrValueList": [
    {
      "valueName": "18"
    }
  ]
}

易错点:categoryId 不要等到保存时才到处找,转写稿里是在点击“添加属性”时收集当前三级分类 ID;categoryLevel 对三级分类固定为 3;attrValueList 里字段是 valueName,不要写成 attrName 或别的同音字段。

属性值行的收集与“站位”思想 ​

为什么需要:属性值可以有多个,表格里每一行对应一个属性值对象。没有行就没有输入框,也就无法收集用户填写的数据。

怎么想:表格 table 绑定的是 attrParams.attrValueList。数组里有一个对象,就渲染一行;点击“添加属性值”时向数组 push 一个空对象,先“占位”,再让用户填写。

怎么做:点击添加属性值时执行类似 attrParams.attrValueList.push({ valueName: '' });表格第二列用 el-input,通过 v-model="row.valueName" 收集当前行数据。删除时用当前行索引 splice(index, 1)。

易错点:不要直接改整个数组导致行错位;新增对象必须符合接口格式,字段名用 valueName;删除只是删本地数组,不需要单独发删除请求,保存时整体提交即可。

编辑模式与查看模式切换 ​

为什么需要:属性值单元格不能永远显示输入框。用户输入完应显示文本,点击文本再进入编辑,这就是“查看模式”和“编辑模式”的切换。

怎么想:一个位置放两个元素:el-input 和 div,用 v-if/v-else 二选一。不能用一个全局布尔值控制所有行,否则一行切换会全部切换;应把标记挂到每个属性值对象上,例如 row.showInput。

怎么做:给属性值对象增加 showInput 字段。el-input 用 v-if="row.showInput",div 用 v-else。输入框失焦时调用 toLook(row),把 row.showInput = false;点击 div 时调用 toEdit(row),把 row.showInput = true。

易错点:showInput 要加到属性值对象的 TS 类型里,设为可选;不要用 v-show 代替 v-if 时忽略 DOM 重建问题;标记必须一行一份,不能共用。

非法情况判断:空值与重复 ​

为什么需要:属性值为空或重复时提交没有意义,也会造成脏数据。转写稿里还提到空值时 div 没有文字撑开,用户甚至点不回去,所以必须拦截。

怎么想:在输入框失焦、准备切回查看模式前判断。空值用 trim() 判断;重复判断要拿当前值和数组里其他项比较,但必须排除当前行自己,否则自己和自己比较永远重复。

怎么做:toLook(row) 里先判断 row.valueName.trim(),为空则提示并 splice 删除当前项;再用 find 查找数组中是否有其他项 valueName 相同,找到则提示重复并删除当前项。只有通过判断才把 row.showInput 设为 false。

易错点:重复判断不排除自身会误判;空格值要用 trim();删除要按当前索引删,不能删错行;判断不通过时不要继续执行切查看模式的语句。

聚焦与 nextTick ​

为什么需要:从 div 切到输入框后,用户希望直接输入,输入框应自动聚焦。

怎么想:聚焦操作要拿到 el-input 组件实例并调用 focus()。但 showInput 刚变为 true 时 DOM 还没渲染完,立即拿 ref 会报 cannot read property focus。

怎么做:给输入框加 ref,在 toEdit 和添加属性值后使用 nextTick(() => { inputRef.value?.focus() }),等响应式更新完成后再聚焦。

易错点:不要在修改 showInput 后立刻同步调用 focus();nextTick 要放在状态修改之后;多行输入框的 ref 要能对应到当前行。

保存、更新与深拷贝 ​

为什么需要:新增和更新共用同一个 POST 接口,有 ID 是更新,无 ID 是新增。保存按钮还要在属性名和属性值至少一项存在时才可用。

怎么想:保存前检查 attrName 和 attrValueList.length,不满足则禁用。更新时把已有行数据合并进 attrParams 展示到场景二,但直接赋值数组会共享引用,修改编辑区会污染表格展示数据。

怎么做:保存按钮加 :disabled,条件为属性名为空或属性值数组长度为 0。更新时用深拷贝,例如 JSON.parse(JSON.stringify(row)) 后再合并,避免 attrValueList 引用共享。保存成功后提示、切回场景 0、重新获取列表;清空表单时也要清掉 id,否则新增会变成更新。

易错点:转写稿中更新后表格数据跟着变,就是浅拷贝导致同一地址;清空时漏清 id 会出现“新增却走更新”;调试时优先看请求参数、返回值,再看组件数据,不要只看页面效果。

SPU与SKU:先分清“标准化产品”和“库存最小单位” ​

SPU 是标准化产品单元,SKU 是库存量最小单位。这两个词不是页面字段,而是电商后台做商品管理时的业务抽象。为什么需要先分清?因为后台如果只按“苹果6手机”建一条数据,就无法表达红色、紫色、蓝色,128、256,安卓、苹果、塞班,以及 1299、6999、899 这些可售组合;如果只按每个组合建数据,又缺少“它们都属于苹果6”的标准化描述。怎么想?把 SPU 理解成“这是什么商品”,把 SKU 理解成“具体哪一款能卖、能记库存”。怎么做?按本节笔记的例子:

  • SPU:苹果6手机
  • 颜色:红色、紫色、蓝色
  • 内存:128、256
  • 系统:安卓、苹果、塞班
  • 价格:1299、6999、899
  • SKU:红色、256、苹果、6999
  • SKU:蓝色、128、塞班、1299

易错点:不要把颜色、内存直接当成 SPU 本身;一个 SPU 可以对应多个 SKU;SKU 要能唯一确定库存和价格组合。价格在实际业务中通常落在 SKU 上,本节按笔记示例理解即可。

2.1 完成SPU静态搭建前,先把属性与属性值收集清楚 ​

SPU 静态搭建不是一上来就写复杂接口,而是先把页面结构、分类、属性、属性值这些基础数据准备好。转写稿里反复强调:新增属性要发请求,参数必须整理成接口需要的形式。为什么需要?因为后台新增属性不是“看到输入框就完事”,而是要把用户填写的数据收集成对象。怎么想?先明确接口字段:categoryId 表示归属哪个三级分类,categoryLevel 表示分类级别,attrName 是属性名,attrValueList 是属性值数组。怎么做?可以先用一个响应式对象站位:

js
const attrParams = {
  categoryId: 61,
  categoryLevel: 3,
  attrName: '颜色',
  attrValueList: [
    { valueName: '黑色' }
  ]
}

属性名最好收集,直接给输入框加 v-model="attrParams.attrName";属性值要按行收集,每一行对应数组里的一个对象。转写稿里说“站位思想”:表格里先有一个对象,才有一行;点“添加属性值”就往 attrValueList 里推一个空对象,再在表格插槽里绑定 row.valueName。易错点:属性值不能为空;属性值不能重复;新增属性值前要判断属性名是否已有,否则“添加属性值”按钮应禁用。三级分类 ID 可以在点击“添加属性”时收集,因为此时分类已经选好。

编辑模式与查看模式:不要用一个布尔值控制所有行 ​

属性值表格里常见两种状态:输入框是编辑模式,div 是查看模式。为什么需要?用户填写时要输入框,失去焦点后要展示文本,点击文本又能回到编辑。怎么想?不能只放一个 input,而要准备 input 和 div 两个元素,用 v-if/v-else 二选一。怎么做?关键是把“是否编辑”的标记挂在每一行的属性值对象上,而不是定义一个全局布尔值:

js
// 每个属性值对象上增加标记
{ valueName: '', isEdit: true }

模板中按当前行判断:

html
<el-input
  v-if="row.isEdit"
  v-model="row.valueName"
  @blur="toLook(row)"
/>
<div v-else @click="toEdit(row)">
  {{ row.valueName }}
</div>

转写稿里调试时发现:一个变量控制所有行,会导致“一个出来全出来,一个没有全没有”。所以必须让每行对象各自带标记。易错点:toLook 里要先判断属性值是否为空、是否重复;为空要提示并删除对应行;重复要提示并阻止进入查看模式。判断重复时要把当前行自己排除,否则“自己跟自己比”一定重复。聚焦要在 nextTick 后做,因为响应式数据刚变化时输入框还没渲染完:

js
nextTick(() => {
  inputRef.value?.focus()
})

更新已有属性:注意深浅拷贝 ​

更新已有属性时,点击“修改”要把当前行数据带入场景一展示。为什么需要?因为用户要在原数据基础上改,而不是重新填。怎么想?把当前属性对象合并到 attrParams,再切换到场景一。怎么做?可以用对象合并,但要注意数组引用。转写稿里出现“改编辑区,表格展示也跟着变”的问题,原因是数组是引用类型,属于浅拷贝。解决方式是对当前行做深拷贝再合并:

js
const rowCopy = JSON.parse(JSON.stringify(row))
attrParams = Object.assign({}, attrParams, rowCopy)

易错点:更新时要保留已有 id,清空表单时也要把 id 清掉,否则下一次新增会被当成更新;删除属性值只是删数组元素,不需要单独发请求;保存按钮要有禁用条件,至少有一个属性值且属性名不为空。调试时优先看 Network 里的请求参数,再看组件数据,最后看页面效果,不要只盯界面。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:属性值「编辑模式 / 查看模式」切换 + 非法情况判断 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>属性值:编辑模式 / 查看模式切换</title>
<style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  table { border-collapse: collapse; width: 560px; }
  th, td { border: 1px solid #dcdfe6; padding: 8px 12px; text-align: left; }
  th { background: #f5f7fa; }
  .cell { height: 32px; }
  .cell input {
    width: 100%; box-sizing: border-box; height: 30px; padding: 0 8px;
    border: 1px solid #409eff; border-radius: 4px; outline: none;
  }
  .view { display: block; min-height: 30px; line-height: 30px; cursor: text; }
  .del { color: #f56c6c; cursor: pointer; border: none; background: none; }
  .tip { color: #f56c6c; margin-left: 12px; }
</style>
</head>
<body>
  <table>
    <thead>
      <tr><th style="width:200px">属性值</th><th style="width:100px">操作</th></tr>
    </thead>
    <tbody id="tbody"></tbody>
  </table>
  <button id="addBtn" style="margin-top:12px">添加属性值</button>
  <span class="tip" id="tip"></span>

<script>
// 每个属性值对象身上挂一个 isInput,就是“编辑/查看模式”的标记
// 有多少行就有多少个标记,行与行之间互不影响
let attrValueList = [
  { valueName: '黑色', isInput: false },
  { valueName: '白色', isInput: false }
];

const tbody = document.getElementById('tbody');
const tipEl = document.getElementById('tip');

function showTip(msg) {
  tipEl.textContent = msg;
  clearTimeout(showTip.timer);
  showTip.timer = setTimeout(() => (tipEl.textContent = ''), 2000);
}

function render() {
  tbody.innerHTML = '';

  attrValueList.forEach((row, index) => {
    const tr = document.createElement('tr');

    // ---------- 第一列:input(编辑模式)与 span(查看模式)二选一 ----------
    const td = document.createElement('td');
    const cell = document.createElement('div');
    cell.className = 'cell';

    if (row.isInput) {
      const input = document.createElement('input');
      input.value = row.valueName;
      // 输入时同步数据,相当于 v-model="row.valueName"
      input.addEventListener('input', e => { row.valueName = e.target.value; });
      // 失焦:先校验,再决定是否切回查看模式
      input.addEventListener('blur', () => handleBlur(index));
      cell.appendChild(input);
      // 响应式数据刚变化时拿不到新 DOM,要等渲染完再 focus
      requestAnimationFrame(() => input.focus());
    } else {
      const span = document.createElement('span');
      span.className = 'view';
      span.textContent = row.valueName;
      // 点击查看模式 -> 切回编辑模式
      span.addEventListener('click', () => { row.isInput = true; render(); });
      cell.appendChild(span);
    }
    td.appendChild(cell);
    tr.appendChild(td);

    // ---------- 第二列:删除 ----------
    const tdOp = document.createElement('td');
    const btn = document.createElement('button');
    btn.className = 'del';
    btn.textContent = '删除';
    // 关键:阻止 mousedown 默认行为,否则 input 先 blur 触发 render,index 就错乱了
    btn.addEventListener('mousedown', e => e.preventDefault());
    btn.addEventListener('click', () => {
      attrValueList.splice(index, 1);
      render();
    });
    tdOp.appendChild(btn);
    tr.appendChild(tdOp);

    tbody.appendChild(tr);
  });
}

// 失焦校验:属性值不能为空、不能重复
function handleBlur(index) {
  const row = attrValueList[index];
  const value = (row.valueName || '').trim();   // 注意 trim,全是空格也算空

  // 1. 非空校验
  if (value === '') {
    showTip('属性值不能为空');
    attrValueList.splice(index, 1);             // 空值直接把这行干掉
    render();
    return;
  }

  // 2. 重复校验:必须排除自己!自己已经在数组里了,不排除一定判成重复
  const isRepeat = attrValueList.some(
    (item, i) => i !== index && item.valueName.trim() === value
  );
  if (isRepeat) {
    showTip('属性值不能重复');
    attrValueList.splice(index, 1);
    render();
    return;
  }

  // 两个校验都通过,才切回查看模式
  row.valueName = value;
  row.isInput = false;
  render();
}

// 点击“添加属性值”:往数组里 push 一个空对象,先占位,才能有行、有 input 去收数据
document.getElementById('addBtn').addEventListener('click', () => {
  attrValueList.push({ valueName: '', isInput: true });
  render();
});

render();
</script>
</body>
</html>

要点解释

  • isInput 必须挂在每一行数据上,不能用一个全局变量控制所有行,否则“一个出来全出来、一个没了全没了”。
  • 字段什么时候加?在 addBtn 点击 push 属性值对象时顺手加上,默认 false(查看模式)。
  • blur 里判断顺序:先非空(trim() 后判空,防止用户只敲空格),再重复,最后才切回查看模式;不通过就直接 splice 把该行删掉并给提示。
  • 重复判断一定要 i !== index 排除自身,因为当前这行已经在数组里了,不排除必然判成重复。
  • focus() 必须等渲染完成后再调(Vue 里对应 nextTick),否则报 Cannot read property 'focus' of undefined。
  • 删除按钮用 mousedown + preventDefault 拦住 input 的 blur,避免删除时先触发校验导致下标错位。

示例二:修改属性 —— 浅拷贝污染表格数据 / 深拷贝解决 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>修改属性:深拷贝</title>
<style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  pre { background: #f5f7fa; padding: 12px; border-radius: 6px; width: 420px; }
  button { margin-right: 12px; }
  #tip { color: #e6a23c; }
</style>
</head>
<body>
  <h3>已有属性列表 attrList(场景 0 表格渲染用)</h3>
  <pre id="listView"></pre>

  <h3>表单收集的参数 attrParams(场景 1 展示用)</h3>
  <pre id="paramsView"></pre>

  <button id="editBtn">点击“修改”最后一条属性</button>
  <button id="changeBtn">把表单里的属性值改成 123</button>
  <p id="tip"></p>

<script>
// 相当于从服务器拿到、渲染在表格里的已有属性数据
const attrList = [
  { attrName: '颜色', attrValueList: [{ valueName: '黑色' }] },
  { attrName: '内存', attrValueList: [{ valueName: '128G' }] }
];

// 新增 / 修改共用的收集参数(接口是同一个,区别只在有没有 id)
let attrParams = {
  categoryId: '',
  categoryLevel: 3,
  attrName: '',
  attrValueList: []
};

function render() {
  document.getElementById('listView').textContent = JSON.stringify(attrList, null, 2);
  document.getElementById('paramsView').textContent = JSON.stringify(attrParams, null, 2);
}

// 点击“修改”:把选中的已有属性合并进 attrParams
document.getElementById('editBtn').addEventListener('click', () => {
  const row = attrList[attrList.length - 1];   // 模拟点了最后一行的修改

  // ❌ 浅拷贝:Object.assign 只拷第一层,attrValueList 还是同一个数组地址
  // Object.assign(attrParams, row);

  // ✅ 深拷贝:先整份复制出一个新地址,再合并,就不会影响表格数据
  Object.assign(attrParams, JSON.parse(JSON.stringify(row)));

  document.getElementById('tip').textContent = '已把已有属性合并进 attrParams';
  render();
});

// 模拟用户在表单里改属性值
document.getElementById('changeBtn').addEventListener('click', () => {
  if (!attrParams.attrValueList.length) {
    document.getElementById('tip').textContent = '请先点“修改”';
    return;
  }
  attrParams.attrValueList[0].valueName = '123';
  document.getElementById('tip').textContent =
    '对比两块数据:上面的 attrList 若跟着变成 123,说明是浅拷贝;没变才是深拷贝';
  render();
});

render();
</script>
</body>
</html>

要点解释

  • 把上面注释掉的 Object.assign(attrParams, row) 放开再点“改成 123”,会看到表格里的数据也一起变了——因为 attrValueList 是引用类型,合并时只复制了地址,两边指向同一个数组。
  • 字符串(attrName)这类基本类型赋值是值拷贝,改了不影响原数据;数组/对象是引用类型,赋值就是浅拷贝,你变我也变。
  • 深拷贝最省事的写法:JSON.parse(JSON.stringify(row)),先转字符串(丢掉地址)再转回对象,得到全新地址。
  • 清空表单数据时别忘了把 id 也清掉,否则新增时带着旧 id 发过去,会被后端当成“更新”,出现“明明点的是添加却提示修改成功”的问题。
  • 清空逻辑建议封装成一个方法,在“取消”和“保存成功”后统一调用,不要一个一个字段手点。

示例三:新增属性 —— 参数收集、按钮禁用、提交与清空 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>新增属性:收集参数并提交</title>
<style>
  body { font-family: system-ui, sans-serif; padding: 24px; }
  input { height: 30px; padding: 0 8px; border: 1px solid #dcdfe6; border-radius: 4px; }
  button { height: 32px; margin-left: 8px; }
  pre { background: #f5f7fa; padding: 12px; border-radius: 6px; width: 460px; white-space: pre-wrap; }
</style>
</head>
<body>
  <label>属性名称:<input id="attrName" placeholder="如:颜色" /></label>
  <button id="saveBtn">保存</button>
  <pre id="out">等待操作…</pre>

<script>
// 三级分类固定信息:给某个三级分类新增属性,level 永远是 3
const categoryId = 61;
const categoryLevel = 3;

// 收集参数的对象,格式必须和接口要求一致
const attrParams = {
  categoryId,
  categoryLevel,
  attrName: '',
  attrValueList: [{ valueName: '18' }]   // 属性值对象数组
};

const out = document.getElementById('out');
const saveBtn = document.getElementById('saveBtn');
const attrNameInput = document.getElementById('attrName');

// 属性名最好收集:它就是个表单元素,直接同步
attrNameInput.addEventListener('input', e => {
  attrParams.attrName = e.target.value;
  refreshBtn();
});

// 保存按钮禁用条件:属性名有值 且 属性值至少有一个
function refreshBtn() {
  const ok = attrParams.attrName.trim() !== '' && attrParams.attrValueList.length > 0;
  saveBtn.disabled = !ok;
}

saveBtn.addEventListener('click', async () => {
  // 发请求前再深拷一份,避免后面清空表单把已发出的数据也改掉
  const data = JSON.parse(JSON.stringify(attrParams));
  out.textContent = '请求参数:\n' + JSON.stringify(data, null, 2);

  // 真实项目里是 POST /admin/product/saveAttrInfo
  // 添加和更新用同一个接口,唯一区别是参数里有没有 id
  const res = await mockRequest(data);
  out.textContent += '\n\n响应:' + JSON.stringify(res);

  if (res.code === 200) {
    out.textContent += '\n提示:' + (data.id ? '修改成功' : '添加成功');
    resetParams();   // 成功后清空数据,切回场景 0,再重新拉一次已有属性列表
  } else {
    out.textContent += '\n提示:操作失败';
  }
});

function mockRequest(data) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ code: 200, data, message: 'OK' }), 300);
  });
}

// 清空数据:取消 / 保存成功后都要走这里
function resetParams() {
  Object.assign(attrParams, {
    attrName: '',
    attrValueList: []
    // id 也要在这里清掉(本例没有 id,真实项目别忘了)
  });
  attrNameInput.value = '';
  refreshBtn();
}

refreshBtn();
</script>
</body>
</html>

要点解释

  • 四步套路:收集参数 → 整理成接口要求的格式 → 发请求 → 成功/失败处理,其中最难的就是“想尽办法把用户填的数据整理成接口要的形状”。
  • categoryLevel 恒为 3(永远给三级分类新增),categoryId 在点“添加属性”那一刻就已经有了,所以可以在添加时或保存时收集,两种都行。
  • 保存按钮必须加禁用:attrName 为空或 attrValueList.length === 0 时不允许提交,否则会发出一堆空数据。
  • 请求前用 JSON.parse(JSON.stringify()) 再拷一份,防止 resetParams() 把已经发出去的数据也一起清掉。
  • 成功提示要区分“添加成功 / 修改成功”,判断依据就是参数里有没有 id。
  • 清空数据封装成 resetParams(),在“取消”和“保存成功”后各调一次,保证每次进来都是干净的场景。

常见错误 ​

  • 点击“添加属性值”后调用 focus() 报 Cannot read properties of undefined → 响应式数据刚变化,DOM/组件实例尚未渲染完成 → 在添加行或切到编辑模式后 await nextTick(),再通过 ref 调 focus()。
  • 点击任意一行编辑,所有行同时变成输入框 → 用一个共享布尔值控制全部行 → 把编辑/查看标记挂到每个属性值对象上,按当前行数据独立控制。
  • 属性值为空或全是空格,失焦后仍切到查看模式,甚至留下空行 → blur 时未先做非法校验 → 先 trim() 判空,提示并删除当前索引行,阻断后续切换逻辑。
  • 重复校验把当前行自己算作重复,导致正常值也被判重 → 校验时未排除当前项自身 → 过滤掉当前索引项后再判断,重复时提示并删除/阻断。
  • 更新回显后,取消修改仍污染场景 0 的表格数据 → 直接把引用类型对象合并/赋值,发生浅拷贝 → 对选中属性做深拷贝后再合并回显,确保“你变我不变”。
  • 更新过一次再新增,提示仍是“更新成功”或提交 id: 0 失败 → 重置表单时只清了名称和属性值,没清 id → 在 reset 中恢复完整初始结构并清空 id。
  • 保存按钮可提交空属性名或空属性值列表 → 禁用条件只判断了其中一个字段 → 同时校验 attrName 和 attrValueList.length。
  • 删除属性值行时误发删除请求 → 把“删除本地待提交行”当成服务端删除 → 仅 splice 当前数组项,保存时统一提交。
  • 提交参数缺少 categoryId/categoryLevel 或取值错误 → 收集时机不对或写死错误 → 在添加/保存前从三级分类取 categoryId,categoryLevel 固定为 3。
  • 接口失败只盯页面效果,排查效率低 → 未先看 Network 的请求参数与响应 → 先确认请求方法、参数结构、返回值,再看组件数据与 UI。

练习 ​

  1. 为属性值表格实现行级编辑/查看切换:点击 DIV 进入编辑,失焦回到查看,并保证各行互不影响。
  2. 在失焦逻辑中加入空值、纯空格、重复值校验;非法时给出提示并删除当前行,验证连续添加多行仍正常。
  3. 完成新增/更新共用保存逻辑:根据是否存在 id 区分提示文案;成功后回到场景 0、重新拉取列表并重置表单。
  4. 复现“更新回显污染表格”的问题,再用深拷贝修复;验收标准是点击取消后场景 0 数据不变。
  5. 完善保存按钮禁用与删除行逻辑,并用 Network 检查提交参数是否包含 categoryId、categoryLevel、attrName、attrValueList。

原视频对照 ​

#视频时长
1完成新增属性的业务44:59
2编辑模式与查看模式的切换18:52
3编辑模式与查看模式切换非法情况判断22:23
4编辑模式与查看模式切换表单元素聚焦10:28
5完成添加属性的业务05:18
6完成更新已有属性的业务23:54

小结 ​

  • 这类业务先按接口结构收集数据,再用 Network 验证参数,不要只看页面效果。
  • 行级编辑状态必须挂在行数据上,共享布尔值一定会造成联动。
  • 非法校验要放在模式切换之前,空值、空格、重复值都要阻断并清理。
  • 编辑回显涉及引用类型时必须切断引用,深拷贝是项目中的真实应用场景。
  • 新增与更新常共用接口,靠 id 区分;重置时要清 id 和完整表单结构。
  • 属性值删除是本地数组操作,保存时统一提交,不应单独发删除请求。
  • 聚焦等依赖 DOM 的动作要等渲染完成,nextTick 是必要步骤。
  • 测试重点覆盖:空值、空格、重复、取消、连续新增/更新、请求参数与返回值。