主题
完成新增属性与属性值业务与SPU与SKU理解(Day5 1/2)
本页对应 day5,共 6 段视频,约 2 小时 5 分。
学习目标
- 能收集并组装新增属性请求参数(
categoryId、categoryLevel、attrName、attrValueList),调用新增/更新共用接口完成新增,并在成功后提示、切回场景 0、重新拉取属性列表。 - 能通过属性值行对象上的布尔标记控制
input与div的显示切换,实现编辑模式与查看模式的逐行切换且各行互不影响。 - 能在失焦切换查看模式前校验属性值不能为空、不能为纯空格、不能重复,并能排除当前行自身后判断重复;非法时给出提示并阻止切换或删除对应空值行。
- 能在新增属性值及点击
div进入编辑模式时,通过nextTick获取输入框实例并调用聚焦方法,实现切换后自动聚焦。 - 能完成属性值添加、删除及保存按钮禁用逻辑,确保属性名和至少一个属性值存在后才允许提交。
- 能点击已有属性进入修改场景,将当前行数据合并到表单并展示,使用深拷贝避免修改时联动表格数据,同时清理
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 里的请求参数,再看组件数据,最后看页面效果,不要只盯界面。
课堂画面






代码示例
示例一:属性值「编辑模式 / 查看模式」切换 + 非法情况判断
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。
练习
- 为属性值表格实现行级编辑/查看切换:点击 DIV 进入编辑,失焦回到查看,并保证各行互不影响。
- 在失焦逻辑中加入空值、纯空格、重复值校验;非法时给出提示并删除当前行,验证连续添加多行仍正常。
- 完成新增/更新共用保存逻辑:根据是否存在
id区分提示文案;成功后回到场景 0、重新拉取列表并重置表单。 - 复现“更新回显污染表格”的问题,再用深拷贝修复;验收标准是点击取消后场景 0 数据不变。
- 完善保存按钮禁用与删除行逻辑,并用 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是必要步骤。 - 测试重点覆盖:空值、空格、重复、取消、连续新增/更新、请求参数与返回值。