主题
SPU管理模块场景切换与搭建场景2的结构(Day6 1/2)
本页对应 day6,共 5 段视频,约 1 小时 51 分。
学习目标
- 能够用场景值控制 SPU 管理模块中“已有 SPU 列表”“添加/更新 SPU”“添加 SKU”三个场景的显示与隐藏,并完成添加、更新、取消按钮的场景切换。
- 能够将添加/更新 SPU 与添加 SKU 拆分为独立子组件,并通过自定义事件实现子组件取消操作后通知父组件切回场景 0。
- 能够搭建添加/更新 SPU 场景的静态表单结构,包括 SPU 名称、品牌、描述、图片墙、销售属性表格及保存/取消按钮。
- 能够定义并调用获取全部品牌、指定 SPU 照片墙、指定 SPU 已有销售属性、全部销售属性四个接口,并补齐对应 TS 类型。
- 能够通过 ref 调用子组件暴露的方法,在更新已有 SPU 时携带 SPU ID 发起四个请求,并将返回数据存储到 ref 中拼装成完整 SPU 数据。
知识点
场景拆解:先分清“页面切换”和“弹窗”
本节笔记把 SPU 管理模块拆成三个场景:场景 0 展示已有 SPU,场景 1 添加 SPU 与更新已有 SPU,场景 2 添加 SKU。为什么需要先拆场景?因为底部卡片区域的结构会整体变化:场景 0 是表格,场景 1 是表单,不能靠零散按钮硬改。怎么想:先看“场景名是否变化”。转写稿里现场判断:添加 SKU 按钮点开后出现的是弹窗,底部场景名不变,所以不要机械地把所有弹窗都做成场景切换;但按笔记仍预留场景 2,便于后续扩展。怎么做:父组件维护场景值,三个区域三选一显示。易错点:场景值不要用布尔值,因为布尔只有真/假两种情况,而这里至少是 0、1、2 三选一。
ts
// 0 展示已有SPU;1 添加/更新SPU;2 预留添加SKU
const scene = ref(0)为什么把场景 1 拆成子组件
为什么需要:场景 1 里有 SPU 名称、品牌、描述、图片墙、销售属性、保存/取消等大量结构,全塞进父组件会“写懵逼”。怎么想:父组件只负责场景切换和表格;添加/更新 SPU 的表单拆成 SPUForm,添加 SKU 拆成 SKUForm。怎么做:建 SPUForm/index.vue、SKUForm/index.vue,父组件引入并用 v-show 控制。转写稿强调用 v-show 而不是频繁 v-if,因为子组件结构多,反复销毁重建不划算。易错点:v-show 隐藏时组件已经挂载,所以不能在子组件 onMounted 里就发“更新用”的四个请求,因为此时还不知道要更新哪条 SPU。
vue
<div v-show="scene === 0">SPU表格</div>
<SPUForm v-show="scene === 1" ref="spuFormRef" :spu="currentSpu" @changeScene="changeScene" />
<SKUForm v-show="scene === 2" />场景 0 到场景 1:添加与更新入口
为什么需要:场景 0 的“添加 SPU”“更新已有 SPU”都要进入场景 1。怎么想:添加只切场景;更新还要把当前行 SPU 数据带给子组件。怎么做:添加按钮执行 scene.value = 1;更新按钮先保存当前行,再切场景,并通过 ref 调子组件初始化方法。易错点:更新按钮必须带 row,否则子组件不知道更新谁;子组件取消时不能直接改父组件变量,要用自定义事件通知父组件切回 0。
ts
const spuFormRef = ref()
const currentSpu = ref<SPU>()
const addSPU = () => { scene.value = 1 }
const updateSPU = (row: SPU) => {
currentSpu.value = row
scene.value = 1
spuFormRef.value.initHasSPU(row)
}
const changeScene = (val: number) => { scene.value = val }ts
// SPUForm 内
const emit = defineEmits<{ (e: 'changeScene', val: number): void }>()
const cancel = () => emit('changeScene', 0)
defineExpose({ initHasSPU })更新已有 SPU:数据不是完整的
为什么需要:转写稿反复提醒,场景 0 拿到的已有 SPU 只有名称、描述等少量字段,并不是完整 SPU。怎么想:点击更新后,至少要补齐四类数据:全部品牌、当前 SPU 的照片墙、当前 SPU 已有销售属性与属性值、全部销售属性。怎么做:在子组件 initHasSPU(spu) 中发四个请求,其中照片墙和已有销售属性要带当前 SPU 的 ID;全部品牌和全部销售属性不带 ID。易错点:两个带 ID 的请求不能在子组件挂载时发,因为挂载时还没有当前 SPU;也不建议父组件发完再层层传参或塞仓库,转写稿认为直接在子组件发更顺。
ts
const initHasSPU = async (spu: SPU) => {
const [trademarkRes, imageRes, hasSaleRes, saleRes] = await Promise.all([
reqAllTrademark(),
reqImageList(spu.id),
reqSpuHasSaleAttr(spu.id),
reqAllSaleAttr()
])
trademarkArr.value = trademarkRes
imageArr.value = imageRes
spuHasSaleAttrArr.value = hasSaleRes
saleAttrArr.value = saleRes
}TS 类型与易错点
为什么需要:这四个接口返回的大多是数组,数组里套对象,对象里还有属性值数组,不写类型很容易“眼花缭乱”。怎么想:先看接口返回,再定 Trademark、图片墙数组、销售属性与属性值、全部销售属性等类型。怎么做:不确定是否必有的字段加 ?,例如新增图片时图片 ID、SPU ID 可能还没有;品牌下拉只展示,ID 可不加问号。易错点:不要为了 TS 类型卡死业务逻辑,转写稿建议先把按钮、请求、数据存储跑通,再补类型;四个请求的结果要存到 ref,最后与传入的半成品 SPU 拼成完整 SPU 供更新表单展示。
一、先明确三个场景,为什么用数字场景值
SPU 管理模块不是一个固定页面:底部卡片切换时,主体结构会变。转写稿里梳理出三个场景:场景 0 展示已有 SPU(含分页,用 el-table);场景 1 添加新 SPU 或更新已有 SPU;场景 2 添加 SKU。为什么需要场景值? 因为三块结构差异大,不能靠一个布尔值表达。怎么想? 父组件只负责“当前显示谁”,具体结构拆成子组件。怎么做? 在父组件定义 scene,用 v-show 控制:
vue
<el-table v-show="scene === 0" />
<SPUForm v-show="scene === 1" />
<SKUForm v-show="scene === 2" />ts
const scene = ref(0)易错点:不要用 true/false 控制三个场景;不要用 v-if,因为子组件结构多,频繁销毁重建不划算,且更新时要通过 ref 调子组件方法。
二、搭建添加/更新 SPU 表单结构
为什么需要拆组件? 添加/更新 SPU 的表单项很多,全塞在父组件里结构容易写乱。怎么想? 把“添加新 SPU 与更新已有 SPU”的结构放进 SPUForm 子组件,父组件只做切换。怎么做? 用 el-form 搭表单项:SPU 名称、SPU 品牌(el-select)、SPU 描述(textarea)、SPU 图片(el-upload 多图上传)、SPU 销售属性(下拉菜单 + 添加销售属性按钮 + el-table)、保存/取消按钮。图片上传要引入 plus 图标;销售属性表格列包括序号、属性名、属性值、操作。销售属性全项目最多三个:颜色、版本、尺码。
vue
<el-form>
<el-form-item label="SPU名称">
<el-input placeholder="请输入SPU名称" />
</el-form-item>
<el-form-item label="SPU品牌">
<el-select><el-option label="华为" value="华为" /></el-select>
</el-form-item>
<el-form-item label="SPU描述">
<el-input type="textarea" />
</el-form-item>
<el-form-item label="SPU图片">
<el-upload action="" :before-upload="" :on-success="" />
</el-form-item>
<el-form-item label="SPU销售属性">
<el-select />
<el-button type="primary" :icon="Plus">添加销售属性</el-button>
<el-table>
<el-table-column type="index" label="序号" width="80" />
<el-table-column label="属性名" width="120" />
<el-table-column label="属性值" width="120" />
<el-table-column label="操作" width="120" />
</el-table>
</el-form-item>
<el-form-item>
<el-button type="primary">保存</el-button>
<el-button @click="cancel">取消</el-button>
</el-form-item>
</el-form>易错点:label 宽度不统一会参差不齐,可统一设置;图片墙是多图上传,不是单图;销售属性最多三个,不能凭空加第四种。
三、父子通信:更新按钮如何把已有 SPU 交给子组件
为什么需要? 点击“更新”时,父组件手里有当前行的已有 SPU,但数据不完整,只有名称、描述等;子组件需要拿到它并继续取完整数据。怎么想? 父组件用 ref 拿到子组件实例,调用子组件暴露的方法。怎么做? 子组件用 defineExpose 暴露 initHasSPU;父组件点击更新时切换 scene = 1,并调用方法:
ts
const spuForm = ref()
const updateSPU = (row: SPU) => {
scene.value = 1
spuForm.value.initHasSPU(row)
}ts
const initHasSPU = (spu: SPU) => {
// 在这里发四个请求,拼完整SPU
}
defineExpose({ initHasSPU })取消按钮不能直接改父组件的 scene,要用自定义事件通知父组件切回场景 0:
ts
const emit = defineEmits(['changeScene'])
const cancel = () => emit('changeScene', 0)易错点:四个请求不要在子组件 onMounted 里发。子组件虽然隐藏但已挂载,此时还没有明确的 SPU ID;应在点击更新、调用 initHasSPU 时发。
四、四个接口与 TS 类型:拼出完整 SPU
为什么需要? 已有 SPU 不是完整数据:品牌下拉要所有品牌;照片墙要按 SPU ID 取;已有销售属性与属性值要按 SPU ID 取;全部销售属性只有颜色、版本、尺码。怎么想? 四个请求返回后拼成完整 SPU。怎么做? 接口分别获取全部品牌、某 SPU 照片墙、某 SPU 已有销售属性与属性值、全部销售属性;参数该带 spuId 的必须带。
ts
export interface Trademark { id: number; tmName: string; logoUrl: string }
export interface ImageItem { id?: number; spuId?: number; imgName: string; imgUrl: string }
export interface SaleAttrValue { id?: number; saleAttrId?: number; saleAttrValueName: string }
export interface SaleAttr { id?: number; saleAttrName: string; spuSaleAttrValueList: SaleAttrValue[] }易错点:新增时 ID 由数据库生成,照片墙的 spuId、销售属性/属性值 ID 可能没有,类型可加 ?;已有数据有 ID 就带。更新和添加的数据格式相近,区别是新增没有 ID。把四个请求结果存起来,再与已有 SPU 拼成完整数据,后续表单回显和保存都依赖它。
课堂画面





代码示例
一、父组件:SPU 管理场景切换(src/views/product/spu/index.vue)
vue
<template>
<div>
<!-- 场景 0:展示已有 SPU -->
<div v-show="scene === 0">
<el-card>
<el-button type="primary" :icon="Plus" @click="addSPU">添加SPU</el-button>
<el-table :data="spuList" border style="margin-top: 10px">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="spuName" label="SPU名称" />
<el-table-column prop="description" label="SPU描述" />
<el-table-column label="操作" width="240">
<template #default="{ row }">
<el-button type="primary" size="small" @click="updateSPU(row)">更新SPU</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<!-- 场景 1:添加新的SPU / 更新已有SPU -->
<SPUForm
v-show="scene === 1"
ref="spuRef"
:spu="currentSPU"
@changeScene="changeScene"
/>
<!-- 场景 2:添加SKU -->
<SKUForm v-show="scene === 2" @changeScene="changeScene" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import SPUForm from './SPUForm/index.vue'
import SKUForm from './SKUForm/index.vue'
// 场景值:0-展示已有SPU 1-添加/更新SPU 2-添加SKU
const scene = ref<number>(0)
const spuRef = ref<any>() // 拿到子组件实例
const currentSPU = ref<any>() // 当前要更新的那条(残缺的)SPU
const spuList = ref<any[]>([]) // 已有SPU列表(前一天的接口)
// 点击「添加SPU」:只切场景
const addSPU = () => {
scene.value = 1
}
// 点击「更新SPU」:切场景 + 传数据 + 调子组件方法发请求
const updateSPU = (row: any) => {
scene.value = 1
currentSPU.value = row
spuRef.value.initHasSPU(row) // 关键:父调子的方法
}
// 子组件通过自定义事件把场景值传回来
const changeScene = (num: number) => {
scene.value = num
}
</script>关键行与易错点
- 场景值用
number而不是布尔:三个场景是三选一,boolean只有真假两种状态,写不下;用0/1/2(或''/a/b)都可以。 - 必须用
v-show,不能随手换成v-if:v-show只是隐藏,子组件一开始就挂载好了,所以spuRef.value第一次点击就能拿到;换成v-if后子组件首次不渲染,spuRef.value是undefined,initHasSPU直接报错。 updateSPU里先切场景、再传数据、最后调方法:调initHasSPU时子组件内部要用spu.id发两个带参请求,所以顺序不能乱。addSPU不调initHasSPU:添加是全新 SPU,没有 ID,不需要发那四个请求。- 三个场景拆成三个子组件,而不是在一个组件里堆三个
div,否则结构太长容易写乱。
二、子组件:场景 1 静态结构 + 取消事件 + 暴露初始化方法(src/views/product/spu/SPUForm/index.vue)
vue
<template>
<el-form label-width="120px">
<el-form-item label="SPU名称">
<el-input v-model="spuObj.spuName" placeholder="请您输入SPU名称" />
</el-form-item>
<el-form-item label="SPU品牌">
<el-select v-model="spuObj.tmId" placeholder="请选择品牌">
<el-option
v-for="tm in trademarkList"
:key="tm.id"
:label="tm.tmName"
:value="tm.id"
/>
</el-select>
</el-form-item>
<el-form-item label="SPU描述">
<el-input v-model="spuObj.description" type="textarea" placeholder="请您输入描述" />
</el-form-item>
<el-form-item label="SPU图片">
<el-upload
action="/api/product/upload"
list-type="picture-card"
:file-list="imageList"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="SPU销售属性">
<el-select placeholder="请选择销售属性" style="width: 200px">
<el-option
v-for="item in baseSaleAttrList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<el-button type="primary" :icon="Plus" style="margin-left: 10px">添加销售属性</el-button>
<el-table style="margin-top: 10px">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column label="属性名" width="120" />
<el-table-column label="属性值" />
<el-table-column label="操作" width="120" />
</el-table>
</el-form-item>
<el-form-item>
<el-button type="primary">保存</el-button>
<el-button @click="cancel">取消</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import {
reqAllTradeMark,
reqImageList,
reqSaleAttrList,
reqBaseSaleAttrList,
} from '@/api/product/spu'
import type {
TradeMark,
ImageList,
SaleAttrList,
BaseSaleAttrList,
} from '@/api/product/spu'
defineProps<{ spu?: any }>()
const emit = defineEmits<{ (e: 'changeScene', num: number): void }>()
// 最终拼出来的完整 SPU
const spuObj = ref<any>({})
const trademarkList = ref<TradeMark[]>([])
const imageList = ref<ImageList>([])
const saleAttrList = ref<SaleAttrList>([])
const baseSaleAttrList = ref<BaseSaleAttrList>([])
// 取消:通知父组件切回场景 0
const cancel = () => {
emit('changeScene', 0)
}
// 父组件点「更新SPU」时调用,参数是那条残缺的已有 SPU
const initHasSPU = async (spu: any) => {
spuObj.value = spu // 先存下名字、描述
// 四个请求:两个不带参、两个必须带 spu.id
const result1 = await reqAllTradeMark()
trademarkList.value = result1
const result2 = await reqImageList(spu.id)
imageList.value = result2
const result3 = await reqSaleAttrList(spu.id)
saleAttrList.value = result3
const result4 = await reqBaseSaleAttrList()
baseSaleAttrList.value = result4
}
// 必须暴露,否则父组件拿不到
defineExpose({ initHasSPU })
</script>关键行与易错点
defineExpose({ initHasSPU })不能漏:<script setup>里定义的变量和方法默认是“关闭”的,父组件通过ref只能拿到被expose出去的东西,不加这一行父组件调spuRef.value.initHasSPU会提示方法不存在。- 四个请求不能写在
onMounted里:这个子组件默认就是挂载状态(v-show),一挂载时根本没有spu.id,两个带参请求必错;必须在父组件点“更新SPU”后,由父组件调方法触发。 - 四个请求都
await,且带参的两个必须用spu.id:照片墙要告诉服务器“要谁的图片”,销售属性要告诉服务器“要哪个 SPU 的”,少传 ID 拿到的就是别人的数据。 - 取消必须走自定义事件:场景值是父组件的数据,子组件不能直接改,只能
emit('changeScene', 0)让父组件改。 label-width="120px"统一写在el-form上,不然各个el-form-item的标签宽度参差不齐,表格也对不齐。- 图片墙是多图上传:要用
el-upload的list-type="picture-card",图标用Plus(需要import { Plus } from '@element-plus/icons-vue'),少这个 import 加号图标就不显示。
三、四个接口与 TS 类型(src/api/product/spu.ts)
ts
import request from '@/utils/request'
import API from '@/api/product/api' // 存放接口地址常量
/* ========== 1. 获取全部品牌(不带参) ========== */
export interface TradeMark {
id: number // 已有品牌一定有 ID,不用加问号
tmName: string
logoUrl: string
}
export type TradeMarkList = TradeMark[]
export const reqAllTradeMark = () =>
request.get<any, TradeMarkList>(API.TRADE_MARK_URL)
/* ========== 2. 获取某个已有 SPU 的照片墙(带 spuId) ========== */
export interface ImageValue {
id?: number // 新增图片的 ID 由数据库生成,所以可选
spuId?: number // 新增 SPU 时还没有 spuId,所以可选
imgName: string
imgUrl: string
}
export type ImageList = ImageValue[]
export const reqImageList = (spuId: number) =>
request.get<any, ImageList>(`${API.IMAGE_LIST_URL}/${spuId}`)
/* ========== 3. 获取某个已有 SPU 的销售属性与属性值(带 spuId) ========== */
export interface SaleAttrValue {
id?: number
saleAttrId?: number
baseSaleAttrId?: number
saleAttrValueName: string
isChecked?: boolean
}
export type SaleAttrValueList = SaleAttrValue[]
export interface SaleAttr {
id?: number
spuId?: number
baseSaleAttrId: number // 颜色=1、版本=2、尺码=3,必须带
saleAttrName: string
spuSaleAttrValueList: SaleAttrValueList
}
export type SaleAttrList = SaleAttr[]
export const reqSaleAttrList = (spuId: number) =>
request.get<any, SaleAttrList>(`${API.SALE_ATTR_URL}/${spuId}`)
/* ========== 4. 获取全部销售属性(全项目只有 3 个,不带参) ========== */
export interface BaseSaleAttr {
id: number // 颜色/版本/尺码,ID 固定,不用加问号
name: string
}
export type BaseSaleAttrList = BaseSaleAttr[]
export const reqBaseSaleAttrList = () =>
request.get<any, BaseSaleAttrList>(API.BASE_SALE_ATTR_URL)关键行与易错点
- “不确定的字段一律加
?”:id、spuId、saleAttrId这些字段在“更新”时有值、在“添加”时由后端生成,写成可选类型才不会在新增时报类型错误。 - 确定的字段不加问号:品牌
TradeMark.id、全部销售属性BaseSaleAttr.id都是库里已有数据,一定有值。 baseSaleAttrId是“属性 ID”,不是“SPU 的 ID”:颜色=1、版本=2、尺码=3,全项目销售属性最多 3 个,所以这个字段必填。- 照片墙返回的是数组,数组里是对象,所以要有
ImageValue和ImageList两个类型;销售属性同理,要有SaleAttrValue/SaleAttrValueList/SaleAttr/SaleAttrList,别只写一个interface。 - 带参接口用模板字符串拼 ID,别把 ID 写成 query 参数之外的
undefined,否则服务器拿不到要查谁。 - 四个请求返回的都是数组,所以
ref初始化时给[],别给{},否则v-for渲染会报错。
常见错误
现象:场景值用
boolean,结果“添加 SPU”和“更新 SPU”只能二选一,切到 SKU 场景就崩。 原因:场景有三个(0 展示 / 1 添加或更新 / 2 添加 SKU),布尔只有两态。 修法:场景值用number(0/1/2)或字符串枚举,父组件统一持有,子组件只读不改。现象:一进页面就在子组件里连发四个请求,照片墙和销售属性全是
undefined或报错。 原因:子组件用的是v-show而非v-if,onMounted在页面初始化就执行了,此时父组件还没把要更新的 SPU 传进来,更没有 SPU 的 ID。 修法:请求不能在onMounted发;由父组件在“更新”按钮回调里通过ref调子组件暴露的方法,在方法内部发请求。现象:父组件
spuRef.value.initHasSPU is not a function,或拿到的是undefined。 原因:<script setup>里的变量和方法默认不对外暴露,ref只能拿到组件实例,拿不到内部方法。 修法:子组件用defineExpose({ initHasSPU })显式导出,父组件const spuRef = ref()同名绑定后再调用。现象:点“更新”后表单里只有名称和描述,品牌下拉是空的、照片墙是空的、销售属性是空的。 原因:场景 0 列表里拿到的 SPU 是“残次品”,只含 id / spuName / description;品牌、照片墙、已有销售属性、全部销售属性这四份数据都要单独请求后拼装。 修法:明确“一个完整 SPU = 列表数据 + 四个接口结果”,在子组件内用
ref分别存trademarkArr / imgArr / spuHasSaleAttrArr / saleAttrArr,拼装后再渲染。现象:点“取消”后场景没切回 0,或者子组件直接改父组件的场景值,控制台警告 / 状态错乱。 原因:子组件想直接修改父组件的数据,违反单向数据流。 修法:子组件
emit('changeScene', 0),父组件监听后修改自己的场景值;defineEmits里把参数类型声明为number。现象:TS 类型一路标红,
Property 'id' does not exist、spuId报错,越写越乱。 原因:没有区分“已有数据”和“新增数据”的字段差异:照片的id、spuId,销售属性值的id、baseSaleAttrId在新增时都不存在。 修法:这类字段一律加?声明为可选;确定由后端生成的(如品牌 id)不加?。先跑通逻辑再补类型,不要一边写结构一边纠结类型。现象:“添加销售属性”的下拉里还能选到该 SPU 已有的属性,出现重复项。 原因:下拉直接用了“全部销售属性”,没有做差集。 修法:下拉数据 = 全部销售属性(颜色/版本/尺码,最多 3 个)过滤掉当前 SPU 已有的;同时加“已满 3 个禁用按钮”的保护。
练习
- 在父组件里用
ref+defineExpose打通“更新”按钮 → 子组件方法的调用链,并在子组件方法里打印父组件传入的 SPU,确认能拿到id。 - 在子组件内并发发出四个请求(全部品牌、该 SPU 照片墙、该 SPU 已有销售属性、全部销售属性),用
Promise.all或顺序await拿到结果并分别存入四个ref,最后拼出一个完整 SPU 对象打印出来。 - 为四个接口补全 TS 类型:注意
ImageItem的id?、spuId?,SaleAttrValue的id?、baseSaleAttrId?,并说明每个可选字段在“新增”场景下为什么可以没有。 - 实现“添加销售属性”下拉的差集逻辑:已有 2 个销售属性时,下拉只剩 1 项;已有 3 项时按钮置灰不可点。
- 实现“取消”按钮:子组件
emit通知父组件把场景值切回 0,并验证再次点“更新”时四个请求会重新发起、数据不残留。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | SPU管理模块场景的切换 | 16:58 |
| 2 | 场景1静态搭建 | 16:07 |
| 3 | 场景切换之更新已有SPU | 32:01 |
| 4 | 书写更新SPU模块四个接口与ts类型 | 34:10 |
| 5 | 书写更新SPU的四个接口并且存储数据 | 12:21 |
小结
- SPU 管理一共三个场景:0 展示列表、1 添加/更新 SPU、2 添加 SKU;场景值必须是能表达三态的
number,不能用布尔。 - 三个场景各自拆成子组件(
SpuForm、SkuForm),父组件只负责“谁显示”和“传什么”,结构才不会写乱。 - 场景 0 拿到的 SPU 是不完整的,只有 id、名称、描述;完整 SPU 必须靠四个接口拼装,这是本模块最核心的数据认知。
- 四个请求必须在“点击更新”之后发,不能在
onMounted发;用ref+defineExpose让父组件调用子组件方法,避免父子来回传参和塞仓库。 - 照片墙和销售属性的 TS 类型必须区分“已有”和“新增”:后端生成的 id 类字段加
?,不确定就加?,宁可宽松也不要卡住逻辑。 - 子组件改父组件状态一律走自定义事件(如
changeScene),不要直接改父级数据。 - 商品销售属性全项目最多 3 个(颜色、版本、尺码),下拉选项要做差集,满 3 个要禁用添加。
- 这个模块的难点不在静态结构,而在接口与数据格式;写之前先看一遍接口返回,比写完再调快得多。