六、项目搭建-商品管理(spu, sku)

573 阅读3分钟

3、SPU管理

3.1 spu和sku的概述
SPU:电商术语,代表的是一个标准化产品单元
​
​
华为公司:品牌名称 华为->产品单元 (类)
SPU组成:
SPU:产品品牌名字
SPU:描述
SPU:公司旗下产品图片介绍
SPU:销售属性[整个项目销售属性一共三个:颜色、版本、尺码]
​
​
SKU:库存量最小单位 (实例)
​
​

image-20230902230103035.png 点击“添加SPU” 弹窗如下:

image-20230902225326136.png

点击列表右侧 “SPU 操作” 中的 加号按钮显示如下:

image-20230902225719229.png

点击列表右侧 “SPU 操作” 中的 预览按钮显示如下:

image-20230902225836835.png

3.2 spu 列表模块搭建和头部三级分类组件的引入
  1. 定义类型 project\src\api\product\spu\type.ts
//服务器全部接口返回的数据类型
export interface ResponseData {
    code: number,
    message: string,
    ok: boolean
}
​
//SPU数据的ts类型:需要修改
export interface SpuData {
    category3Id: string | number,
    id?: number,
    spuName: string,
    tmId: number | string,
    description: string,
    spuImageList: null | SpuImg[]
    spuSaleAttrList: null | SaleAttr[],
​
}
//数组:元素都是已有SPU数据类型
export type Records = SpuData[];
//定义获取已有的SPU接口返回的数据ts类型
export interface HasSpuResponseData extends ResponseData {
    data: {
        records: Records,
        total: number,
        size: number,
        current: number,
        searchCount: boolean,
        pages: number
    }
}
​
​
//品牌数据的TS类型
export interface Trademark {
    id: number,
    tmName: string,
    logoUrl: string
}
//品牌接口返回的数据ts类型
export interface AllTradeMark extends ResponseData {
    data: Trademark[]
}
​
//商品图片的ts类型
export interface SpuImg {
    id?: number,
    imgName?: string,
    imgUrl?: string
    createTime?: string,
    updateTime?: string,
    spuId?: number,
    name?: string,
    url?: string
​
}
//已有的SPU的照片墙数据的类型
export interface SpuHasImg extends ResponseData {
    data: SpuImg[]
}
​
​
//已有的销售属性值对象ts类型
export interface SaleAttrValue {
    "id"?: number,
    "createTime"?: null,
    "updateTime"?: null,
    "spuId"?: number,
    "baseSaleAttrId": number | string,
    "saleAttrValueName": string,
    "saleAttrName"?: string,
    "isChecked"?: null
}
//存储已有的销售属性值数组类型
export type SpuSaleAttrValueList = SaleAttrValue[];
​
//销售属性对象ts类型
export interface SaleAttr {
    "id"?: number,
    "createTime"?: null,
    "updateTime"?: null,
    "spuId"?: number,
    "baseSaleAttrId": number | string,
    "saleAttrName": string,
    "spuSaleAttrValueList": SpuSaleAttrValueList
    flag?: boolean,
    saleAttrValue?: string,
}
//SPU已有的销售属性接口返回数据ts类型
export interface SaleAttrResponseData extends ResponseData {
    data: SaleAttr[]
}
​
​
​
//已有的全部SPU的返回数据ts类型
export interface HasSaleAttr {
    id: number,
    name: string
}
​
export interface HasSaleAttrResponseData extends ResponseData {
    data: HasSaleAttr[]
}
​
​
​
export interface Attr {
    "attrId": number | string,//平台属性的ID
    "valueId": number | string,//属性值的ID
}
​
export interface saleArr {
    "saleAttrId": number | string,//属性ID
    "saleAttrValueId": number | string,//属性值的ID
}
export interface SkuData {
    "category3Id": string | number,//三级分类的ID
    "spuId": string | number,//已有的SPU的ID
    "tmId": string | number,//SPU品牌的ID
    "skuName": string,//sku名字
    "price": string | number,//sku价格
    "weight": string | number,//sku重量
    "skuDesc": string,//sku的描述
    "skuAttrValueList"?: Attr[],
    "skuSaleAttrValueList"?: saleArr[]
    "skuDefaultImg": string,//sku图片地址
}
​
​
//获取SKU数据接口的ts类型
export interface SkuInfoData extends ResponseData {
    data: SkuData[]
} 
  1. 编写接口 project\src\api\product\spu\index.ts
//SPU管理模块的接口
import request from "@/utils/request";
import type { SkuInfoData, SkuData, SpuData, HasSpuResponseData, AllTradeMark, SpuHasImg, SaleAttrResponseData, HasSaleAttrResponseData } from './type';
enum API {
  //获取已有的SPU的数据
  HASSPU_URL = '/admin/product/',
  //获取全部品牌的数据
  ALLTRADEMARK_URL = '/admin/product/baseTrademark/getTrademarkList',
  //获取某个SPU下的全部的售卖商品的图片数据
  IMAGE_URL = '/admin/product/spuImageList/',
  //获取某一个SPU下全部的已有的销售属性接口地址
  SPUHASSALEATTR_URL = '/admin/product/spuSaleAttrList/',
  //获取整个项目全部的销售属性[颜色、版本、尺码]
  ALLSALEATTR_URL = '/admin/product/baseSaleAttrList',
  //追加一个新的SPU
  ADDSPU_URL = '/admin/product/saveSpuInfo',
  //更新已有的SPU
  UPDATESPU_URL = '/admin/product/updateSpuInfo',
  //追加一个新增的SKU地址
  ADDSKU_URL = '/admin/product/saveSkuInfo',
  //查看某一个已有的SPU下全部售卖的商品
  SKUINFO_URL = '/admin/product/findBySpuId/',
  //删除已有的SPU
  REMOVESPU_URL = '/admin/product/deleteSpu/'
}
//获取某一个三级分类下已有的SPU数据
export const reqHasSpu = (page: number, limit: number, category3Id: string | number) => request.get<any, HasSpuResponseData>(API.HASSPU_URL + `${page}/${limit}?category3Id=${category3Id}`);
//获取全部的SPU的品牌的数据
export const reqAllTradeMark = () => request.get<any, AllTradeMark>(API.ALLTRADEMARK_URL);
//获取某一个已有的SPU下全部商品的图片地址
export const reqSpuImageList = (spuId: number) => request.get<any, SpuHasImg>(API.IMAGE_URL + spuId)
//获取某一个已有的SPU拥有多少个销售属性
export const reqSpuHasSaleAttr = (spuId: number) => request.get<any, SaleAttrResponseData>(API.SPUHASSALEATTR_URL + spuId)
//获取全部的销售属性
export const reqAllSaleAttr = () => request.get<any, HasSaleAttrResponseData>(API.ALLSALEATTR_URL);
//添加一个新的SPU的
//更新已有的SPU接口
//data:即为新增的SPU|或者已有的SPU对象
export const reqAddOrUpdateSpu = (data: SpuData) => {
  //如果SPU对象拥有ID,更新已有的SPU
  if (data.id) {
    return request.post<any, any>(API.UPDATESPU_URL, data)
  } else {
    return request.post<any, any>(API.ADDSPU_URL, data);
  }
}
//添加SKU的请求方法
export const reqAddSku = (data: SkuData) => request.post<any, any>(API.ADDSKU_URL, data);
​
//获取SKU数据
export const reqSkuList = (spuId: number | string) => request.get<any, SkuInfoData>(API.SKUINFO_URL + spuId);
​
//删除已有的SPU
export const reqRemoveSpu = (spuId: number | string) => request.delete<any, any>(API.REMOVESPU_URL + spuId);
​
  1. 在 project\src\views\product\spu\index.vue 页面实现

    <template>
      <div>
           <!-- 三级分类 -->
            <Category :scene="scene"></Category>
          <el-card style="margin:10px 0px">
            <!-- v-if|v-show:都可以实现显示与隐藏 -->
                <div v-show="scene == 0">
                    <el-button @click="addSpu" type="primary" size="default" icon="Plus"
                        :disabled="categoryStore.c3Id ? false : true">添加SPU</el-button>
                    <!-- 展示已有SPU数据 -->
                    <el-table style="margin: 10px 0px;" border :data="records">
                        <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                        <el-table-column label="SPU名称" prop="spuName"></el-table-column>
                        <el-table-column label="SPU描述" prop="description" show-overflow-tooltip></el-table-column>
                        <el-table-column label="SPU操作">
                            <!-- row:即为已有的SPU对象 -->
                            <template #="{ row, $index }">
                                <el-button type="primary" size="small" icon="Plus" title="添加SKU"
                                    @click="addSku(row)"></el-button>
                                <el-button type="primary" size="small" icon="Edit" title="修改SPU"
                                    @click="updateSpu(row)"></el-button>
                                <el-button type="primary" size="small" icon="View" title="查看SKU列表"
                                    @click="findSku(row)"></el-button>
                                <el-popconfirm :title="`你确定删除${row.spuName}?`" width="200px" @confirm="deleteSpu(row)">
                                    <template #reference>
                                        <el-button type="primary" size="small" icon="Delete" title="删除SPU"></el-button>
                                    </template>
                                </el-popconfirm>
    ​
                            </template>
                        </el-table-column>
                    </el-table>
                    <!-- 分页器 -->
                    <el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[3, 5, 7, 9]"
                        :background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="total"
                        @current-change="getHasSpu" @size-change="changeSize" />
                </div>
    ​
           </el-card>
      </div>
    </template>
    <script setup lang="ts">
    import type { HasSpuResponseData, Records, SkuInfoData, SkuData } from '@/api/product/spu/type'
    import { ref, watch, onBeforeUnmount } from 'vue';
    import { reqHasSpu, reqSkuList, reqRemoveSpu } from '@/api/product/spu';
    //引入分类的仓库
    import useCategoryStore from '@/store/modules/category';
    import type { SpuData } from '@/api/product/spu/type'
    ​
    ​
    import { ElMessage } from 'element-plus';
    let categoryStore = useCategoryStore();
    //场景的数据
    let scene = ref<number>(0); //0:显示已有SPU  1:添加或者修改已有SPU 2:添加SKU的结构
    //分页器默认页码
    let pageNo = ref<number>(1);
    //每一页展示几条数据
    let pageSize = ref<number>(3);
    //存储已有的SPU的数据
    let records = ref<Records>([]);
    //存储已有SPU总个数
    let total = ref<number>(0);
    ​
    //监听三级分类ID变化
    watch(() => categoryStore.c3Id, () => {
        //当三级分类发生变化的时候清空对应的数据
        records.value = [];
        //务必保证有三级分类ID
        if (!categoryStore.c3Id) return;
        getHasSpu();
    });
    ​
    //此方法执行:可以获取某一个三级分类下全部的已有的SPU
    const getHasSpu = async (pager = 1) => {
        //修改当前页码
        pageNo.value = pager;
        let result: HasSpuResponseData = await reqHasSpu(pageNo.value, pageSize.value, categoryStore.c3Id);
        if (result.code == 200) {
            records.value = result.data.records;
            total.value = result.data.total;
        }
    }
    ​
    //分页器下拉菜单发生变化的时候触发
    const changeSize = () => {
        getHasSpu();
    }
    ​
    ​
    ​
    //添加新的SPU按钮的回调
    const addSpu = () => {
        //切换为场景1:添加与修改已有SPU结构->SpuForm
        scene.value = 1;
        
    }
    //添加SKU按钮的回调
    const addSku = (row: SpuData) => {
        //点击添加SKU按钮切换场景为2
        scene.value = 2;
        
    }
    ​
    //修改已有的SPU的按钮的回调
    const updateSpu = (row: SpuData) => {
        //切换为场景1:添加与修改已有SPU结构->SpuForm
        scene.value = 1;
    ​
    }
    ​
    //查看SKU列表的数据
    const findSku = async (row: SpuData) => {
        let result: SkuInfoData = await reqSkuList((row.id as number));
        if (result.code == 200) {
            skuArr.value = result.data;
            //对话框显示出来
            show.value = true;
        }
    }
    ​
    //删除已有的SPU按钮的回调
    const deleteSpu = async (row: SpuData) => {
        let result: any = await reqRemoveSpu((row.id as number));
        if (result.code == 200) {
            ElMessage({
                type: 'success',
                message: '删除成功'
            });
            //获取剩余SPU数据
            getHasSpu(records.value.length > 1 ? pageNo.value : pageNo.value - 1)
        } else {
            ElMessage({
                type: 'error',
                message: '删除失败'
            })
        }
    }
    ​
    </script>
    <style lang="scss" scoped>
    ​
    ​
    </style>
3.3 新增和修改spu,sku 组件的封装,并完成整个spu模块的搭建
  1. project\src\views\product\spu\spuForm.vue

    <template>
        <el-form label-width="100px">
            <el-form-item label="SPU名称">
                <el-input placeholder="请你输入SPU名称" v-model="SpuParams.spuName"></el-input>
            </el-form-item>
            <el-form-item label="SPU品牌">
                <el-select v-model="SpuParams.tmId">
                    <el-option v-for="(item, index) in  AllTradeMark" :key="item.id" :label="item.tmName"
                        :value="item.id"></el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="SPU描述">
                <el-input type="textarea" placeholder="请你输入SPU描述" v-model="SpuParams.description"></el-input>
            </el-form-item>
            <el-form-item label="SPU图片">
                <!-- v-model:fileList->展示默认图片 
                     action:上传图片的接口地址
                     list-type:文件列表的类型
                -->
                <el-upload v-model:file-list="imgList" action="/api/admin/product/fileUpload" list-type="picture-card"
                    :on-preview="handlePictureCardPreview" :on-remove="handleRemove" :before-upload="handlerUpload">
                    <el-icon>
                        <Plus />
                    </el-icon>
                </el-upload>
                <el-dialog v-model="dialogVisible">
                    <img w-full :src="dialogImageUrl" alt="Preview Image" style="width:100%;height: 100%;" />
                </el-dialog>
            </el-form-item>
            <el-form-item label="SPU销售属性">
                <!-- 展示销售属性的下拉菜单 -->
                <el-select v-model="saleAttrIdAndValueName"
                    :placeholder="unSelectSaleAttr.length ? `还未选择${unSelectSaleAttr.length}个` : '无'">
                    <el-option :value="`${item.id}:${item.name}`" v-for="(item, index) in unSelectSaleAttr" :key="item.id"
                        :label="item.name"></el-option>
                </el-select>
                <el-button @click="addSaleAttr" :disabled="saleAttrIdAndValueName ? false : true" style="margin-left:10px"
                    type="primary" size="default" icon="Plus">添加属性</el-button>
                <!-- table展示销售属性与属性值的地方 -->
                <el-table border style="margin:10px 0px" :data="saleAttr">
                    <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                    <el-table-column label="销售属性名字" width="120px" prop="saleAttrName"></el-table-column>
                    <el-table-column label="销售属性值">
                        <!-- row:即为当前SPU已有的销售属性对象 -->
                        <template #="{ row, $index }">
                            <el-tag style="margin:0px 5px" @close="row.spuSaleAttrValueList.splice(index, 1)"
                                v-for="(item, index) in row.spuSaleAttrValueList" :key="row.id" class="mx-1" closable>
                                {{ item.saleAttrValueName }}
                            </el-tag>
                            <el-input @blur="toLook(row)" v-model="row.saleAttrValue" v-if="row.flag == true"
                                placeholder="请你输入属性值" size="small" style="width:100px"></el-input>
                            <el-button @click="toEdit(row)" v-else type="primary" size="small" icon="Plus"></el-button>
                        </template>
                    </el-table-column>
                    <el-table-column label="操作" width="120px">
                        <template #="{ row, $index }">
                            <el-button type="primary" size="small" icon="Delete"
                                @click="saleAttr.splice($index, 1)"></el-button>
                        </template>
                    </el-table-column>
                </el-table>
            </el-form-item>
            <el-form-item>
                <el-button :disabled="saleAttr.length > 0 ? false : true" type="primary" size="default"
                    @click="save">保存</el-button>
                <el-button type="primary" size="default" @click="cancel">取消</el-button>
            </el-form-item>
        </el-form>
    </template>
    ​
    <script setup lang="ts">
    import type { SpuData } from '@/api/product/spu/type'
    import { ref, computed } from 'vue';
    import { reqAllTradeMark, reqSpuImageList, reqSpuHasSaleAttr, reqAllSaleAttr, reqAddOrUpdateSpu } from '@/api/product/spu'
    import type { SaleAttrValue, HasSaleAttr, SaleAttr, SpuImg, Trademark, AllTradeMark, SpuHasImg, SaleAttrResponseData, HasSaleAttrResponseData } from '@/api/product/spu/type';
    import { ElMessage } from 'element-plus';
    let $emit = defineEmits(['changeScene']);
    //点击取消按钮:通知父组件切换场景为1,展示有的SPU的数据
    const cancel = () => {
        $emit('changeScene',{flag:0,params:'update'});
    }
    //存储已有的SPU这些数据
    let AllTradeMark = ref<Trademark[]>([]);
    //商品图片
    let imgList = ref<SpuImg[]>([]);
    //已有的SPU销售属性
    let saleAttr = ref<SaleAttr[]>([]);
    //全部销售属性
    let allSaleAttr = ref<HasSaleAttr[]>([]);
    //控制对话框的显示与隐藏
    let dialogVisible = ref<boolean>(false);
    //存储预览图片地址
    let dialogImageUrl = ref<string>('')
    //存储已有的SPU对象
    let SpuParams = ref<SpuData>({
        category3Id: "",//收集三级分类的ID
        spuName: "",//SPU的名字
        description: "",//SPU的描述
        tmId: '',//品牌的ID
        spuImageList: [],
        spuSaleAttrList: [],
    });
    //将来收集还未选择的销售属性的ID与属性值的名字
    let saleAttrIdAndValueName = ref<string>('')
    //子组件书写一个方法
    const initHasSpuData = async (spu: SpuData) => {
        //存储已有的SPU对象,将来在模板中展示
        SpuParams.value = spu;
        //spu:即为父组件传递过来的已有的SPU对象[不完整]
        //获取全部品牌的数据
        let result: AllTradeMark = await reqAllTradeMark();
        //获取某一个品牌旗下全部售卖商品的图片
        let result1: SpuHasImg = await reqSpuImageList((spu.id as number));
        //获取已有的SPU销售属性的数据
        let result2: SaleAttrResponseData = await reqSpuHasSaleAttr((spu.id as number));
        //获取整个项目全部SPU的销售属性
        let result3: HasSaleAttrResponseData = await reqAllSaleAttr();
        //存储全部品牌的数据
        AllTradeMark.value = result.data;
        //SPU对应商品图片,将获取到的每一张图片属性进行映射成组件规定的,组件要求的图片的name就是name,我们的是 imageName
        imgList.value = result1.data.map(item => {
            return {
                name: item.imgName,
                url: item.imgUrl
            }
        })
        //存储已有的SPU的销售属性
        saleAttr.value = result2.data;
        //存储全部的销售属性
        allSaleAttr.value = result3.data;
    }
    //照片墙点击预览按钮的时候触发的钩子
    const handlePictureCardPreview = (file: any) => {
        dialogImageUrl.value = file.url;
        //对话框弹出来
        dialogVisible.value = true;
    }
    //照片墙删除文件钩子
    const handleRemove = () => {
        console.log(123);
    }
    //照片钱上传成功之前的钩子约束文件的大小与类型
    const handlerUpload = (file: any) => {
        if (file.type == 'image/png' || file.type == 'image/jpeg' || file.type == 'image/gif') {
            if (file.size / 1024 / 1024 < 3) {
                return true;
            } else {
                ElMessage({
                    type: 'error',
                    message: '上传文件务必小于3M'
                })
                return false;
            }
        } else {
            ElMessage({
                type: 'error',
                message: '上传文件务必PNG|JPG|GIF'
            })
            return false;
        }
    }
    ​
    //计算出当前SPU还未拥有的销售属性
    let unSelectSaleAttr = computed(() => {
        //全部销售属性:颜色、版本、尺码
        //已有的销售属性:颜色、版本
        let unSelectArr = allSaleAttr.value.filter(item => {
            return saleAttr.value.every(item1 => {
                return item.name != item1.saleAttrName;
            });
        })
        return unSelectArr;
    })
    ​
    //添加销售属性的方法
    const addSaleAttr = () => {
        /*
        "baseSaleAttrId": number,
        "saleAttrName": string,
        "spuSaleAttrValueList": SpuSaleAttrValueList
        */
        const [baseSaleAttrId, saleAttrName] = saleAttrIdAndValueName.value.split(':');
        //准备一个新的销售属性对象:将来带给服务器即可
        let newSaleAttr: SaleAttr = {
            baseSaleAttrId,
            saleAttrName,
            spuSaleAttrValueList: []
        }
        //追加到数组当中
        saleAttr.value.push(newSaleAttr);
        //清空收集的数据
        saleAttrIdAndValueName.value = '';
    ​
    }
    ​
    //属性值按钮的点击事件
    const toEdit = (row: SaleAttr) => {
        //点击按钮的时候,input组件不就不出来->编辑模式
        row.flag = true;
        row.saleAttrValue = ''
    }
    //表单元素失却焦点的事件回调
    const toLook = (row: SaleAttr) => {
        //整理收集的属性的ID与属性值的名字
        const { baseSaleAttrId, saleAttrValue } = row;
        //整理成服务器需要的属性值形式
        let newSaleAttrValue: SaleAttrValue = {
            baseSaleAttrId,
            saleAttrValueName: (saleAttrValue as string)
        }
    ​
        //非法情况判断
        if ((saleAttrValue as string).trim() == '') {
            ElMessage({
                type: 'error',
                message: '属性值不能为空的'
            })
            return;
        }
        //判断属性值是否在数组当中存在
        let repeat = row.spuSaleAttrValueList.find(item => {
            return item.saleAttrValueName == saleAttrValue;
        })
    ​
        if (repeat) {
            ElMessage({
                type: 'error',
                message: '属性值重复'
            })
            return;
        }
    ​
    ​
        //追加新的属性值对象
        row.spuSaleAttrValueList.push(newSaleAttrValue);
        //切换为查看模式
        row.flag = false;
    }
    ​
    //保存按钮的回调
    const save = async () => {
        //整理参数
        //发请求:添加SPU|更新已有的SPU
        //成功
        //失败
        //1:照片墙的数据
        SpuParams.value.spuImageList = imgList.value.map((item: any) => {
            return {
                imgName: item.name,//图片的名字
                imgUrl: (item.response && item.response.data) || item.url
            }
        });
        //2:整理销售属性的数据
        SpuParams.value.spuSaleAttrList = saleAttr.value;
        let result = await reqAddOrUpdateSpu(SpuParams.value);
        if (result.code == 200) {
            ElMessage({
                type: 'success',
                message: SpuParams.value.id ? '更新成功' : '添加成功'
            })
            //通知父组件切换场景为0
            $emit('changeScene',{flag:0,params:SpuParams.value.id?'update':'add'});
        } else {
            ElMessage({
                type: 'success',
                message: SpuParams.value.id ? '更新成功' : '添加成功'
            })
        }
    ​
    ​
    }
    ​
    //添加一个新的SPU初始化请求方法
    const initAddSpu = async (c3Id: number | string) => {
        //清空数据
        Object.assign(SpuParams.value, {
            category3Id: "",//收集三级分类的ID
            spuName: "",//SPU的名字
            description: "",//SPU的描述
            tmId: '',//品牌的ID
            spuImageList: [],
            spuSaleAttrList: [],
        });
        //清空照片
        imgList.value = [];
        //清空销售属性
        saleAttr.value = [];
        saleAttrIdAndValueName.value  = '';
        //存储三级分类的ID
        SpuParams.value.category3Id = c3Id;
        //获取全部品牌的数据
        let result: AllTradeMark = await reqAllTradeMark();
        let result1: HasSaleAttrResponseData = await reqAllSaleAttr();
        //存储数据
        AllTradeMark.value = result.data;
        allSaleAttr.value = result1.data;
    }
    //对外暴露 便于父组件调用进而初始化子组件需要的数据
    defineExpose({ initHasSpuData, initAddSpu })
    </script>
    ​
    <style scoped></style>
    
  2. project\src\views\product\spu\skuForm.vue

    <template>
        <el-form label-width="100px">
            <el-form-item label="SKU名称">
                <el-input placeholder="SKU名称" v-model="skuParams.skuName"></el-input>
            </el-form-item>
            <el-form-item label="价格(元)">
                <el-input placeholder="价格(元)" type="number" v-model="skuParams.price"></el-input>
            </el-form-item>
            <el-form-item label="重量(g)">
                <el-input placeholder="重量(g)" type="number" v-model="skuParams.weight"></el-input>
            </el-form-item>
            <el-form-item label="SKU描述">
                <el-input placeholder="SKU描述" type="textarea" v-model="skuParams.skuDesc"></el-input>
            </el-form-item>
            <el-form-item label="平台属性">
                <el-form :inline="true">
                    <el-form-item v-for="(item, index) in attrArr" :key="item.id" :label="item.attrName">
                        <el-select v-model="item.attrIdAndValueId">
                            <el-option :value="`${item.id}:${attrValue.id}`" v-for="(attrValue, index) in item.attrValueList"
                                :key="attrValue.id" :label="attrValue.valueName"></el-option>
                        </el-select>
                    </el-form-item>
                </el-form>
            </el-form-item>
            <el-form-item label="销售属性">
                <el-form :inline="true">
                    <el-form-item :label="item.saleAttrName" v-for="(item, index) in saleArr" :key="item.id">
                        <el-select v-model="item.saleIdAndValueId">
                            <el-option :value="`${item.id}:${saleAttrValue.id}`"
                                v-for="(saleAttrValue, index) in item.spuSaleAttrValueList" :key="saleAttrValue.id"
                                :label="saleAttrValue.saleAttrValueName"></el-option>
                        </el-select>
                    </el-form-item>
                </el-form>
            </el-form-item>
            <el-form-item label="图片名称">
                <el-table border :data="imgArr" ref="table">
                    <el-table-column type="selection" width="80px" align="center"></el-table-column>
                    <el-table-column label="图片">
                        <template #="{ row, $index }">
                            <img :src="row.imgUrl" alt="" style="width:100px;height: 100px;">
                        </template>
                    </el-table-column>
                    <el-table-column label="名称" prop="imgName"></el-table-column>
                    <el-table-column label="操作">
                        <template #="{ row, $index }">
                            <el-button type="primary" size="small" @click="handler(row)">设置默认</el-button>
                        </template>
                    </el-table-column>
                </el-table>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" size="default" @click="save">保存</el-button>
                <el-button type="primary" size="default" @click="cancel">取消</el-button>
            </el-form-item>
        </el-form>
    </template>
    ​
    <script setup lang="ts">
    //引入请求API
    import { reqAttr } from '@/api/product/attr';
    import { reqSpuImageList, reqSpuHasSaleAttr, reqAddSku } from '@/api/product/spu';
    import type { SkuData } from '@/api/product/spu/type'
    import { ElMessage } from 'element-plus';
    import { ref, reactive } from 'vue';
    //平台属性
    let attrArr = ref<any>([]);
    //销售属性
    let saleArr = ref<any>([]);
    //照片的数据
    let imgArr = ref<any>([]);
    //获取table组件实例
    let table = ref<any>();
    //收集SKU的参数
    let skuParams = reactive<SkuData>({
        //父组件传递过来的数据
        "category3Id": "",//三级分类的ID
        "spuId": "",//已有的SPU的ID
        "tmId": "",//SPU品牌的ID
        //v-model收集
        "skuName": "",//sku名字
        "price": "",//sku价格
        "weight": "",//sku重量
        "skuDesc": "",//sku的描述
    ​
        "skuAttrValueList": [//平台属性的收集
        ],
        "skuSaleAttrValueList": [//销售属性
        ],
        "skuDefaultImg": "",//sku图片地址
    })
    //当前子组件的方法对外暴露
    const initSkuData = async (c1Id: number | string, c2Id: number | string, spu: any) => {
        //收集数据
        skuParams.category3Id = spu.category3Id;
        skuParams.spuId = spu.id;
        skuParams.tmId = spu.tmId;
        //获取平台属性
        let result: any = await reqAttr(c1Id, c2Id, spu.category3Id);
        //获取对应的销售属性
        let result1: any = await reqSpuHasSaleAttr(spu.id);
        //获取照片墙的数据
        let result2: any = await reqSpuImageList(spu.id);
        //平台属性
        attrArr.value = result.data;
        //销售属性
        saleArr.value = result1.data;
        //图片
        imgArr.value = result2.data;
    }
    //取消按钮的回调
    const cancel = () => {
        $emit('changeScene', { flag: 0, params: '' });
    }
    ​
    //设置默认图片的方法回调
    const handler = (row: any) => {
        //点击的时候,全部图片的的复选框不勾选
        imgArr.value.forEach((item: any) => {
            table.value.toggleRowSelection(item, false);
        });
        //选中的图片才勾选
        table.value.toggleRowSelection(row, true);
        //收集图片地址
        skuParams.skuDefaultImg = row.imgUrl;
    }
    //对外暴露方法
    defineExpose({
        initSkuData
    });
    ​
    //保存按钮的方法
    const save = async () => {
        //整理参数
        //平台属性
        skuParams.skuAttrValueList = attrArr.value.reduce((prev: any, next: any) => {
            if (next.attrIdAndValueId) {
                let [attrId, valueId] = next.attrIdAndValueId.split(':');
                prev.push({
                    attrId,
                    valueId
                })
            }
            return prev;
        }, []);
        //销售属性
        skuParams.skuSaleAttrValueList = saleArr.value.reduce((prev: any, next: any) => {
            if (next.saleIdAndValueId) {
                let [saleAttrId, saleAttrValueId] = next.saleIdAndValueId.split(':');
                prev.push({
                    saleAttrId, saleAttrValueId
                })
            }
            return prev;
        }, []);
        //添加SKU的请求
        let result: any = await reqAddSku(skuParams);
        if (result.code == 200) {
            ElMessage({
                type: 'success',
                message: '添加SKU成功'
            });
            //通知父组件切换场景为零
            $emit('changeScene',{flag:0,params:''})
        } else {
            ElMessage({
                type: 'error',
                message: '添加SKU失败'
            })
        }
    ​
    }
    //自定义事件的方法
    let $emit = defineEmits(['changeScene']);
    </script>
    ​
    <style scoped></style>
    
  3. project\src\views\product\spu\index.vue

<template>
    <div>
        <!-- 三级分类 -->
        <Category :scene="scene"></Category>
        <el-card style="margin:10px 0px">
            <!-- v-if|v-show:都可以实现显示与隐藏 -->
            <div v-show="scene == 0">
                <el-button @click="addSpu" type="primary" size="default" icon="Plus"
                    :disabled="categoryStore.c3Id ? false : true">添加SPU</el-button>
                <!-- 展示已有SPU数据 -->
                <el-table style="margin: 10px 0px;" border :data="records">
                    <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                    <el-table-column label="SPU名称" prop="spuName"></el-table-column>
                    <el-table-column label="SPU描述" prop="description" show-overflow-tooltip></el-table-column>
                    <el-table-column label="SPU操作">
                        <!-- row:即为已有的SPU对象 -->
                        <template #="{ row, $index }">
                            <el-button type="primary" size="small" icon="Plus" title="添加SKU"
                                @click="addSku(row)"></el-button>
                            <el-button type="primary" size="small" icon="Edit" title="修改SPU"
                                @click="updateSpu(row)"></el-button>
                            <el-button type="primary" size="small" icon="View" title="查看SKU列表"
                                @click="findSku(row)"></el-button>
                            <el-popconfirm :title="`你确定删除${row.spuName}?`" width="200px" @confirm="deleteSpu(row)">
                                <template #reference>
                                    <el-button type="primary" size="small" icon="Delete" title="删除SPU"></el-button>
                                </template>
                            </el-popconfirm>
​
                        </template>
                    </el-table-column>
                </el-table>
                <!-- 分页器 -->
                <el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[3, 5, 7, 9]"
                    :background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="total"
                    @current-change="getHasSpu" @size-change="changeSize" />
            </div>
            <!-- 添加SPU|修改SPU子组件 -->
            <SpuForm ref="spu" v-show="scene == 1" @changeScene="changeScene"></SpuForm>
            <!-- 添加SKU的子组件 -->
            <SkuForm ref="sku" v-show="scene == 2" @changeScene="changeScene"></SkuForm>
            <!-- dialog对话框:展示已有的SKU数据 -->
            <el-dialog v-model="show" title="SKU列表">
                <el-table border :data="skuArr">
                    <el-table-column label="SKU名字" prop="skuName"></el-table-column>
                    <el-table-column label="SKU价格" prop="price"></el-table-column>
                    <el-table-column label="SKU重量" prop="weight"></el-table-column>
                    <el-table-column label="SKU图片">
                        <template #="{ row, $index }">
                            <img :src="row.skuDefaultImg" style="width: 100px;height: 100px;">
                        </template>
                    </el-table-column>
                </el-table>
            </el-dialog>
        </el-card>
    </div>
</template>
​
<script setup lang="ts">
import type { HasSpuResponseData, Records, SkuInfoData, SkuData } from '@/api/product/spu/type'
import { ref, watch, onBeforeUnmount } from 'vue';
import { reqHasSpu, reqSkuList, reqRemoveSpu } from '@/api/product/spu';
//引入分类的仓库
import useCategoryStore from '@/store/modules/category';
import type { SpuData } from '@/api/product/spu/type'
import SpuForm from './spuForm.vue';
import SkuForm from './skuForm.vue';
import { ElMessage } from 'element-plus';
let categoryStore = useCategoryStore();
//场景的数据
let scene = ref<number>(0); //0:显示已有SPU  1:添加或者修改已有SPU 2:添加SKU的结构
//分页器默认页码
let pageNo = ref<number>(1);
//每一页展示几条数据
let pageSize = ref<number>(3);
//存储已有的SPU的数据
let records = ref<Records>([]);
//存储已有SPU总个数
let total = ref<number>(0);
//获取子组件实例SpuForm
let spu = ref<any>();
//获取子组件实例SkuForm
let sku = ref<any>();
//存储全部的SKU数据
let skuArr = ref<SkuData[]>([]);
let show = ref<boolean>(false);
//监听三级分类ID变化
watch(() => categoryStore.c3Id, () => {
    //当三级分类发生变化的时候清空对应的数据
    records.value = [];
    //务必保证有三级分类ID
    if (!categoryStore.c3Id) return;
    getHasSpu();
});
​
//此方法执行:可以获取某一个三级分类下全部的已有的SPU
const getHasSpu = async (pager = 1) => {
    //修改当前页码
    pageNo.value = pager;
    let result: HasSpuResponseData = await reqHasSpu(pageNo.value, pageSize.value, categoryStore.c3Id);
    if (result.code == 200) {
        records.value = result.data.records;
        total.value = result.data.total;
    }
}
//分页器下拉菜单发生变化的时候触发
const changeSize = () => {
    getHasSpu();
}
​
//添加新的SPU按钮的回调
const addSpu = () => {
    //切换为场景1:添加与修改已有SPU结构->SpuForm
    scene.value = 1;
    //点击添加SPU按钮,调用子组件的方法初始化数据
     //spu 就是子组件SpuForm的实列,然后调用子组件的方法,注意子组件的显示与隐藏要用 v-show , 不能用v-if 否则获取不到子组件的实列
    spu.value.initAddSpu(categoryStore.c3Id);
}
//修改已有的SPU的按钮的回调
const updateSpu = (row: SpuData) => {
    //切换为场景1:添加与修改已有SPU结构->SpuForm
    scene.value = 1;
    //调用子组件实例方法获取完整已有的SPU的数据
    //spu 就是子组件SpuForm的实列,然后调用子组件的方法,注意子组件的显示与隐藏要用 v-show , 不能用v-if 否则获取不到子组件的实列
    spu.value.initHasSpuData(row);
}
​
//子组件SpuForm绑定自定义事件:目前是让子组件通知父组件切换场景为0
const changeScene = (obj: any) => {
    //子组件Spuform点击取消变为场景0:展示已有的SPU
    scene.value = obj.flag;
    if (obj.params == 'update') {
        //更新留在当前页
        getHasSpu(pageNo.value);
    } else {
        //添加留在第一页
        getHasSpu();
    }
}
​
//添加SKU按钮的回调
const addSku = (row: SpuData) => {
    //点击添加SKU按钮切换场景为2
    scene.value = 2;
    //调用子组件的方法初始化添加SKU的数据
     //sku 就是子组件SkuForm的实列,然后调用子组件的方法,注意子组件的显示与隐藏要用 v-show , 不能用v-if 否则获取不到子组件的实列
    sku.value.initSkuData(categoryStore.c1Id, categoryStore.c2Id, row);
}
​
//查看SKU列表的数据
const findSku = async (row: SpuData) => {
    let result: SkuInfoData = await reqSkuList((row.id as number));
    if (result.code == 200) {
        skuArr.value = result.data;
        //对话框显示出来
        show.value = true;
    }
}
​
//删除已有的SPU按钮的回调
const deleteSpu = async (row: SpuData) => {
    let result: any = await reqRemoveSpu((row.id as number));
    if (result.code == 200) {
        ElMessage({
            type: 'success',
            message: '删除成功'
        });
        //获取剩余SPU数据
        getHasSpu(records.value.length > 1 ? pageNo.value : pageNo.value - 1)
    } else {
        ElMessage({
            type: 'error',
            message: '删除失败'
        })
    }
}
​
//路由组件销毁前,情况仓库关于分类的数据
onBeforeUnmount(() => {
    categoryStore.$reset();
})
</script>
​
<style scoped></style>

4、SKU管理

4.1 功能展示

image-20230902231309113.png

image-20230902231041003.png

image-20230902231211091.png

4.2 sku 模块的搭建
  1. 新建sku所有模块所用到的数据类型 project\src\api\product\sku\type.ts

    export interface ResponseData {
        code: number,
        message: string,
        ok: boolean
    }
    //定义SKU对象的ts类型
    export interface Attr {
        id?:number
        "attrId": number | string,//平台属性的ID
        "valueId": number | string,//属性值的ID
    }
    export interface saleArr {
        id?:number,
        "saleAttrId": number | string,//属性ID
        "saleAttrValueId": number | string,//属性值的ID
    }
    export interface SkuData {
        "category3Id"?: string | number,//三级分类的ID
        "spuId"?: string | number,//已有的SPU的ID
        "tmId"?: string | number,//SPU品牌的ID
        "skuName"?: string,//sku名字
        "price"?: string | number,//sku价格
        "weight"?: string | number,//sku重量
        "skuDesc"?: string,//sku的描述
        "skuAttrValueList"?: Attr[],
        "skuSaleAttrValueList"?: saleArr[]
        "skuDefaultImg"?: string,//sku图片地址
        isSale?: number,//控制商品的上架与下架
        id?: number
    }
    ​
    //获取SKU接口返回的数据ts类型
    export interface SkuResponseData extends ResponseData {
        data: {
            records: SkuData[],
            "total": number,
            "size": number,
            "current": number,
            "orders": [],
            "optimizeCountSql": boolean,
            "hitCount": boolean,
            "countId": null,
            "maxLimit": null,
            "searchCount": boolean,
            "pages": number
    ​
        }
    }
    ​
    //获取SKU商品详情接口的ts类型
    export interface SkuInfoData extends ResponseData {
        data: SkuData
    }
    
  1. 新增sku模块所需要的接口 project\src\api\product\sku\index.ts

    //SKU模块接口管理
    import request from "@/utils/request";
    import type { SkuResponseData, SkuInfoData } from './type'
    //枚举地址
    enum API {
        //获取已有的商品的数据-SKU
        SKU_URL = '/admin/product/list/',
        //上架
        SALE_URL = '/admin/product/onSale/',
        //下架的接口
        CANCELSALE_URL = '/admin/product/cancelSale/',
        //获取商品详情的接口
        SKUINFO_URL = '/admin/product/getSkuInfo/',
        //删除已有的商品
        DELETESKU_URL = '/admin/product/deleteSku/'
    }
    //获取商品SKU的接口
    export const reqSkuList = (page: number, limit: number) => request.get<any, SkuResponseData>(API.SKU_URL + `${page}/${limit}`)
    //已有商品上架的请求
    export const reqSaleSku = (skuId: number) => request.get<any, any>(API.SALE_URL + skuId);
    //下架的请求
    export const reqCancelSale = (skuId: number) => request.get<any, any>(API.CANCELSALE_URL + skuId);
    //获取商品详情的接口
    export const reqSkuInfo = (skuId: number) => request.get<any, SkuInfoData>(API.SKUINFO_URL + skuId);
    //删除某一个已有的商品
    export const reqRemoveSku = (skuId: number) => request.delete<any, any>(API.DELETESKU_URL + skuId)
    
    1. 页面实现 project\src\views\product\sku\index.vue

      <template>
          <el-card>
              <el-table border style="margin: 10px 0px;" :data="skuArr">
                  <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                  <el-table-column label="名称" show-overflow-tooltip width="150px" prop="skuName"></el-table-column>
                  <el-table-column label="描述" show-overflow-tooltip width="150px" prop="skuDesc"></el-table-column>
                  <el-table-column label="图片" width="150px">
                      <template #="{ row, $index }">
                          <img :src="row.skuDefaultImg" alt="" style="width: 100px;height: 100px;">
                      </template>
                  </el-table-column>
                  <el-table-column label="重量" width="150px" prop="weight"></el-table-column>
                  <el-table-column label="价格" width="150px" prop="price"></el-table-column>
                  <el-table-column label="操作" width="250px" fixed="right">
                      <template #="{ row, $index }">
                          <el-button type="primary" size="small" :icon="row.isSale == 1 ? 'Bottom' : 'Top'"
                              @click="updateSale(row)"></el-button>
                          <el-button type="primary" size="small" icon="Edit" @click="updateSku"></el-button>
                          <el-button type="primary" size="small" icon="InfoFilled" @click="findSku(row)"></el-button>
                          <el-popconfirm :title="`你确定要删除${row.skuName}?`" width="200px" @confirm="removeSku(row.id)">
                              <template #reference>
                                  <el-button type="primary" size="small" icon="Delete"></el-button>
                              </template>
                          </el-popconfirm>
                      </template>
                  </el-table-column>
              </el-table>
              <el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[10, 20, 30, 40]"
                  :background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="total" @current-change="getHasSku"
                  @size-change="handler" />
              <!-- 抽屉组件:展示商品详情 -->
              <el-drawer v-model="drawer">
                  <!-- 标题部分 -->
                  <template #header>
                      <h4>查看商品的详情</h4>
                  </template>
                  <template #default>
                      <el-row style="margin:10px 0px;">
                          <el-col :span="6">名称</el-col>
                          <el-col :span="18">{{ skuInfo.skuName }}</el-col>
                      </el-row>
                      <el-row style="margin:10px 0px;">
                          <el-col :span="6">描述</el-col>
                          <el-col :span="18">{{ skuInfo.skuDesc }}</el-col>
                      </el-row>
                      <el-row style="margin:10px 0px;">
                          <el-col :span="6">价格</el-col>
                          <el-col :span="18">{{ skuInfo.price }}</el-col>
                      </el-row>
                      <el-row style="margin:10px 0px;">
                          <el-col :span="6">平台属性</el-col>
                          <el-col :span="18">
                              <el-tag style="margin:5px;" v-for="item in skuInfo.skuAttrValueList" :key="item.id">{{
                                  item.valueName }}</el-tag>
                          </el-col>
                      </el-row>
                      <el-row style="margin:10px 0px;">
                          <el-col :span="6">销售属性</el-col>
                          <el-col :span="18">
                              <el-tag style="margin:5px;" v-for="item in skuInfo.skuSaleAttrValueList" :key="item.id">{{
                                  item.saleAttrValueName }}</el-tag>
                          </el-col>
                      </el-row>
                      <el-row style="margin:10px 0px;">
                          <el-col :span="6">商品图片</el-col>
                          <el-col :span="18">
                              <el-carousel :interval="4000" type="card" height="200px">
                                  <el-carousel-item v-for="item in skuInfo.skuImageList" :key="item.id">
                                      <img :src="item.imgUrl" alt="" style="width:100%;height: 100%;">
                                  </el-carousel-item>
                              </el-carousel>
                          </el-col>
                      </el-row>
                  </template>
              </el-drawer>
          </el-card>
      </template><script setup lang="ts">
      import { ref, onMounted } from 'vue';
      //引入请求
      import { reqSkuList, reqSaleSku, reqCancelSale, reqSkuInfo, reqRemoveSku } from '@/api/product/sku'
      //引入ts类型
      import type { SkuResponseData, SkuData, SkuInfoData } from '@/api/product/sku/type';
      import { ElMessage } from 'element-plus';
      //分页器当前页码
      let pageNo = ref<number>(1);
      //每一页展示几条数据
      let pageSize = ref<number>(10);
      let total = ref<number>(0);
      let skuArr = ref<SkuData[]>([]);
      //控制抽屉显示与隐藏的字段
      let drawer = ref<boolean>(false);
      let skuInfo = ref<any>({});
      //组件挂载完毕
      onMounted(() => {
          getHasSku();
      });
      const getHasSku = async (pager = 1) => {
          //当前分页器的页码
          pageNo.value = pager;
          let result: SkuResponseData = await reqSkuList(pageNo.value, pageSize.value);
          if (result.code == 200) {
              total.value = result.data.total;
              skuArr.value = result.data.records;
          }
      }
      //分页器下拉菜单发生变化触发
      const handler = (pageSizes: number) => {
          getHasSku();
      }
      ​
      //商品的上架与下架的操作
      const updateSale = async (row: SkuData) => {
          //如果当前商品的isSale==1,说明当前商品是上架的额状态->更新为下架
          //否则else情况与上面情况相反
          if (row.isSale == 1) {
              //下架操作
              await reqCancelSale((row.id as number));
              //提示信息
              ElMessage({ type: 'success', message: '下架成功' });
              //发请求获取当前更新完毕的全部已有的SKU
              getHasSku(pageNo.value);
      ​
          } else {
              //下架操作
              await reqSaleSku((row.id as number));
              //提示信息
              ElMessage({ type: 'success', message: '上架成功' });
              //发请求获取当前更新完毕的全部已有的SKU
              getHasSku(pageNo.value);
          }
      }
      //更新已有的SKU
      const updateSku = () => {
          ElMessage({ type: 'success', message: '程序员在努力的更新中....' })
      }
      //查看商品详情按钮的回调
      const findSku = async (row: SkuData) => {
          //抽屉展示出来
          drawer.value = true;
          //获取已有商品详情数据
          let result: SkuInfoData = await reqSkuInfo((row.id as number));
          //存储已有的SKU
          skuInfo.value = result.data;
      }
      //删除某一个已有的商品
      const removeSku = async (id: number) => {
          //删除某一个已有商品的情况
          let result: any = await reqRemoveSku(id);
          if (result.code == 200) {
              //提示信息
              ElMessage({ type: 'success', message: '删除成功' });
              //获取已有全部商品
              getHasSku(skuArr.value.length > 1 ? pageNo.value : pageNo.value - 1);
          } else {
              //删除失败
              ElMessage({ type: 'error', message: '系统数据不能删除' });
          }
      }
      </script><style scoped>
      .el-carousel__item h3 {
          color: #475669;
          opacity: 0.75;
          line-height: 200px;
          margin: 0;
          text-align: center;
      }
      ​
      .el-carousel__item:nth-child(2n) {
          background-color: #99a9bf;
      }
      ​
      .el-carousel__item:nth-child(2n + 1) {
          background-color: #d3dce6;
      }
      </style>用户管理模块