使用到element-plus的card、button、table、pagination等组件:src/views/product/trademark/index.vue
添加品牌
src/api/product/trademark/index.ts
// 书写品牌管理模块接口
import request from '@/utils/request'
import type { TrademarkResponeData } from './type'
// 品牌管理模块接口地址
enum API {
// 获取已有品牌接口
TRADEMARK_URL = '/admin/product/baseTrademark/'
}
// 获取已有品牌的接口方法
// page:获取第几页 ---默认第一页
// limit:获取几个已有品牌的数据
export const reqHasTrademark = (page: number, limit: number) => request.get(API.TRADEMARK_URL + `${page}/${limit}`)
src/api/product/trademark/type.ts
export interface ResponeData {
code: number
message: string
ok: boolean
}
// 已有的品牌的ts数据类型
export interface Trademark {
id: number
tmName: string
logoUrl: string
}
// 包含全部品牌数据的ts类型
export type Records = Trademark[]
// 获取的已有全部品牌的数据ts类型
export interface TrademarkResponeData extends ResponeData {
data: {
records: Records
total: number
size: number
current: number
orders: []
optimizeCountSql: boolean
hitCount: boolean
countId: null
maxLimit: null
searchCount: boolean
pages: number
}
}
src/views/product/trademark/index.vue
table-column:默认展示数据用div,通过prop属性展示数据;如果需要自定义列的内容,可以使用插槽#来展示内容。
......
......
给pagination组件标签添加current-change、size-change事件,书写对应方法,在当前页码和下拉菜单(每页展示的数据条数)发生变化时触发getHasTrademark回调,重新获取数据进行展示。
// 分页器当前页码发生变化的时候触发
// 对于当前页码发生变化自定义事件,组件pagination父组件回传了数据(当前的页码)
// const changePageNo = () =>{
// // 当前页码发生变化的时候再次发送请求获取对应已有品牌数据展示
// getHasTrademark()
// }
// 当下拉菜单发生变化的时候触发此方法
// 这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
// 当前每一页的数据发生变化的时候,当前页码归1
getHasTrademark()
}
src/api/product/trademark/index.ts
......
enum API {
......
// 添加品牌
ADDTRADEMARK_URL = '/admin/product/baseTrademark/save',
// 修改已有品牌
UPDATETRADEMARK_URL = '/admin/product/baseTrademark/update'
}
// 添加与修改已有品牌接口方法
export const reqAddOrUpdateTrademark = (data: Trademark) => {
// 修改已有品牌的数据
if (data.id) {
return request.put(API.UPDATETRADEMARK_URL, data)
} else {
// 新增品牌
return request.post(API.ADDTRADEMARK_URL, data)
}
}
src/views/product/trademark/index.vue
图片大小计算:返回的rawFile.size是字节,1024字节 = 1K ,1024K = 1M, 1024M = 1G,
1024G = 1T
PS:表单校验步骤及说明可参考该文章 2.5:【Vue3+Ts项目】硅谷甄选 — 路由配置+登录模块+layout组件+路由鉴权-CSDN博客
......
添加品牌
......
......
取消
确认
import { ElMessage, UploadProps } from 'element-plus'
import { ref, onMounted, reactive, nextTick } from 'vue'
import { reqHasTrademark, reqAddOrUpdateTrademark } from '@/api/product/trademark'
import type { Records, TrademarkResponeData, Trademark } from '@/api/product/trademark/type'
......
// 控制对话框显示与隐藏
let dialogFormVisible = ref(false)
// 定义收集新增品牌数据
let trademarkParams = reactive({
tmName: '',
logoUrl: ''
})
// 获取el-form组件实例
let formRef = ref()
......
// 添加品牌按钮的回调
const addTrademark = () => {
//对话框显示
dialogFormVisible.value = true
// 清空收集数据
trademarkParams.id = 0
trademarkParams.tmName = ''
trademarkParams.logoUrl = ''
// 第一种写法:ts的问号语法
// formRef.value?.clearValidate('tmName')
// formRef.value?.clearValidate('logoUrl')
nextTick(() => {
formRef.value.clearValidate('tmName')
formRef.value.clearValidate('logoUrl')
})
}
// 修改已有品牌按钮的回调
// row:row即为当前已有品牌
const updateTrademark = (row: Trademark) => {
// 清空校验规则错误提示信息
nextTick(() => {
formRef.value.clearValidate('tmName')
formRef.value.clearValidate('logoUrl')
})
//对话框显示
dialogFormVisible.value = true;
Object.assign(trademarkParams, row)
}
//对话框底部取消按钮
const cancel = () => {
//对话框隐藏
dialogFormVisible.value = false;
}
const confirm = async () => {
// 在你发请求之前,要对于整个表单进行校验
// 调用这个方法进行全部表单校验,如果校验全部通过,再执行后面的语句
await formRef.value.validate()
let result: any = await reqAddOrUpdateTrademark(trademarkParams)
if (result.code === 200) {
// 关闭对话框
dialogFormVisible.value = false;
// 弹出提示信息
ElMessage({
type: 'success',
message: trademarkParams.id ? '修改品牌成功' : '添加品牌成功'
})
// 再次发请求获取已有全部的品牌数据
getHasTrademark(trademarkParams.id ? pageNo.value : 1)
} else {
// 添加品牌失败
ElMessage({
type: 'error',
message: trademarkParams.id ? '修改品牌失败' : '添加品牌失败'
})
// 关闭对话框
dialogFormVisible.value = false;
}
}
// 上传图片组件 -> 上传图片之前触发的钩子函数
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
// 钩子是在上传成功之前触发,上传文件之前可以约束文件类型与大小
// 要求:上传文件格式png|jpg|gif 4M
if (rawFile.type == 'image/png' || rawFile.type == 'image/jpeg' || rawFile.type == 'image/gif') {
if (rawFile.size / 1024 / 1024 < 4) {
return true
} else {
ElMessage({
type: 'error',
message: '上传文件大小小于4M'
})
return false
}
} else {
ElMessage.error({
type: 'error',
message: '上传文件格式务必PNG|JPG|GIF'
})
return false
}
}
// 图片上传成功钩子
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
// response:即当前这次上传图片post请求服务器返回的数据
// 收集上传图片的地址,添加一个新的品牌的时候带给服务器
trademarkParams.logoUrl = response.data
// 图片上传成功,清除掉对应图片校验错误提示信息
formRef.value.clearValidate('logoUrl')
}
// 品牌名称自定义校验规则方法
const validatorTmName = (rule: any, value: any, callBack: any) => {
// 是表单元素触发blur时候,会触发此方法
// 自定义校验规则
if (value.trim().length >= 2) {
callBack()
} else {
// 校验未通过返回的错误提示信息
callBack(new Error('品牌名称位数大于等于两位'))
}
}
// 品牌LOGO图片的自定义校验规则方法
const validatorLogoUrl = (rule: any, value: any, callBack: any) => {
// 如果图片上传
if (value) {
callBack()
} else {
callBack(new Error('LOGO图片务必上传'))
}
}
// 表单校验规则对象
const rules = {
// required:这个字段务必校验,表单前面出来五角星
// trigger:代表触发校验规则时机(blur、change)
tmName: [
{ required: true, trigger: 'blur', validator: validatorTmName }
],
logoUrl: [
{ required: true, validator: validatorLogoUrl }
]
}
使用element-plus气泡确认框( Popconfirm)来弹出删除提示。
src/views/product/trademark/index.vue
import { reqHasTrademark, reqAddOrUpdateTrademark, reqDeleteTrademark } from '@/api/product/trademark'
// 气泡确认框确定按钮的回调
const removeTradeMark = async (id: number) => {
// 点击确认按钮删除已有品牌请求
let result = await reqDeleteTrademark(id)
if (result.code === 200) {
// 删除成功提示信息
ElMessage({
type: 'success',
message: '删除品牌成功'
})
// 再次获取已有的品牌数据
getHasTrademark(trademarkArr.value.length > 1 ? pageNo.value : pageNo.value - 1)
} else {
ElMessage({
type: 'error',
message: '删除品牌失败'
})
}
}
三级分类在后续SPU管理模块也会使用到,所以将三级分类封装成一个全局组件更方便使用。
使用el-select下拉菜单实现三级分类全局组件的静态搭建:src/components/Category/index.vue
注册全局组件:src/components/index.ts
import Category from './Category/index.vue'
// 全局组件对象
const allGlobalComponents: any = {
Category,
}
PS:封装全局组件具体的实现可参考此文章:vue3---自定义插件注册全局对象-CSDN博客
引入三级分类全局组件+使用el-table展示属性相关数据,实现属性管理组件的静态搭建:rc/views/product/attr/index.vue
添加属性
分类全局组件挂载时获取一级分类,将获取到的一级分类数据和ID存储在仓库中(方便父组件使用分类ID获取属性相关数据), 通过一级分类的ID获取二级分类,二级分类的ID获取三级分类。
① 接口定义
src/api/product/attr/index.ts
// 这里书写属性相关的API文件
import request from '@/utils/request'
import type { CategoryResponseData } from './type'
// 属性管理模块接口地址
enum API {
// 获取一级分类接口地址
C1_URL = '/admin/product/getCategory1',
// 获取二级分类接口地址
C2_URL = '/admin/product/getCategory2/',
// 获取三级分类接口地址
C3_URL = '/admin/product/getCategory3/',
}
// 获取一级分类的接口方法
export const reqC1 = () => request.get(API.C1_URL)
// 获取二级分类的接口方法
export const reqC2 = (category1: number | string) => request.get(API.C2_URL + category1)
// 获取三级分类的接口方法
export const reqC3 = (category2: number | string) => request.get(API.C3_URL + category2)
② 数据ts类型定义
src/api/product/attr/type.ts
// 分类相关的数据ts类型
export interface ResponseData {
code: number
message: string
ok: boolean
}
// 分类ts类型
export interface CategoryObj {
id: number | string
name: string
category1Id?: number
category2Id?: number
}
// 相应的分类接口返回数据类型
export interface CategoryResponseData extends ResponseData {
data: CategoryObj[]
}
① 创建小仓库
src/store/modules/attr/category.ts
// 商品分类全局组件的小仓库
import { defineStore } from 'pinia'
import { reqC1, reqC2, reqC3 } from '@/api/product/attr'
import type { CategoryResponseData } from '@/api/product/attr/type'
import type { CategoryState } from './types/type'
let useCategoryStore = defineStore('Category', {
state: (): CategoryState => {
return {
// 存储一级分类的数据
c1Arr: [],
// 存储一级分类的ID
c1Id: '',
// 存储对应一级分类下二级分类的数据
c2Arr: [],
// 存储二级分类的ID
c2Id: '',
// 存储三级分类的数据
c3Arr: [],
// 存储三级分类的ID
c3Id: ''
}
},
actions: {
// 获取一级分类的方法
async getC1() {
// 发请求获取一级分类的数据
let result: CategoryResponseData = await reqC1()
if (result.code === 200) {
this.c1Arr = result.data
}
},
// 获取二级分类的方法
async getC2() {
// 获取对应一级分类下的二级分类的数据
let result: CategoryResponseData = await reqC2(this.c1Id)
if (result.code === 200) {
this.c2Arr = result.data
}
},
// 获取三级分类的方法
async getC3() {
let result: CategoryResponseData = await reqC3(this.c2Id)
if (result.code === 200) {
this.c3Arr = result.data
}
}
},
getters: {}
})
export default useCategoryStore
② state数据ts类型定义
src/store/modules/types/type.ts
import type { CategoryObj } from '@/api/product/attr/type'
// 定义分类仓库state对象的ts类型
export interface CategoryState {
c1Id: string | number
c1Arr: CategoryObj[]
c2Arr: CategoryObj[]
c2Id: string | number
c3Arr: CategoryObj[]
c3Id: string | number
}
src/components/Category/index.vue
PS:当父组件传递scene为1时,下拉菜单select禁用,scene为0时,下拉菜单select正常使用。(Category中select组件需加上 :disabled="scene === 1 ? true : false")
提升开发效率的小Tips①:解析格式化JSON的网站(方便查看数据的结构):JSON在线解析及格式化验证 - JSON.cn
① 接口定义
src/api/product/attr/index.ts
import type { ......, AttrResponeData, Attr } from './type'
enum API {
......
// 获取分类下已有属性与属性值
ATTR_URL = '/admin/product/attrInfoList/',
// 添加或修改已有的属性的接口
ADDORUPDATEATTR_URL = '/admin/product/saveAttrInfo',
//删除某一个已有的属性
DELETEATTR_URL = '/admin/product/deleteAttr/',
}
// 获取对应分类下已有的属性与属性值接口
export const reqAttr = (category1Id: string | number, category2Id: string | number, category3Id: string | number) => request.get(API.ATTR_URL + `${category1Id}/${category2Id}/${category3Id}`)
// 新增或修改已有属性的接口
export const reqAddOrUpdateAttr = (data: Attr) => request.post(API.ADDORUPDATEATTR_URL, data)
//删除某一个已有的属性业务
export const reqRemoveAttr = (attrId: number) => request.delete(API.DELETEATTR_URL + attrId)
② 数据ts类型定义
src/api/product/attr/type.ts
// 属性与属性值的ts类型
// 属性值对象的ts类型
export interface AttrValue {
id?: number
valueName: string
attrId?: number
flag: boolean
}
// 存储每一个属性值的数组类型
export type AttrValueList = AttrValue[]
// 属性对象的ts类型
export interface Attr {
id?: number
attrName: string
categoryId: number | string
categoryLevel: number
attrValueList: AttrValueList
}
// 存储每一个属性对象的数组ts类型
export type AttrList = Attr[]
// 属性接口返回的数据ts类型
export interface AttrResponeData extends ResponseData {
data: AttrList
}
src/views/product/attr/index.vue
属性管理业务包括:展示数据、添加/修改数据、删除数据。
- 数据展示:通过三级分类下拉菜单存储的分类ID请求属性与属性值数据,使用el-table进行展示。
- 添加/修改数据:通过scene作为切换标识,进行数据展示与添加/修改数据页面切换,然后收集属性与属性值数据,发请求进行保存。
- 删除数据:使用el-popconfirm进行删除确认发请求。
添加属性
{{ item.valueName
}}
添加属性值
取消
{{ row.valueName }}
保存
取消
Object.assign:Object.assign为浅拷贝,如果修改属性时,直接使用Object.assign(attrParams, row)将已有的属性对象赋值给attrParams对象,会出现修改了属性之后点击取消按钮返回列表时,属性还是修改了;可以使用JSON方法进行深拷贝Object.assign(attrParams, JSON.parse(JSON.stringify(row)))。(关于深拷贝和浅拷贝的区别可以参考文章:js中深拷贝和浅拷贝的理解,它们的区别是什么-CSDN博客)
SPU:电商术语,代表的是一个标准化产品单元。(类)
SPU组成:产品品牌名称+描述+产品图片介绍+销售属性【整个项目销售属性一共三个:颜色、版本、尺码】
例如,华为公司的品牌名称是华为,华为就是一个产品单元。
SKU:库存最小单位。(实例)
① 接口定义
src/api/product/spu/index.ts
// SPU管理模块的接口
import request from "@/utils/request";
import { SpuData, HasSpuResponeData, AllTrademark, SpuHasImg, SaleAttrResponseData, HasSaleAttrResponseData, SkuData, SkuInfoData } 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: number | string) => request.get(API.HASSPU_URL + `${page}/${limit}?category3Id=${category3Id}`)
// 获取全部的SPU的品牌的数据
export const reqAllTrademark = () => request.get(API.ALLTRADEMARK_URL)
// 获取某一个已有的SPU下全部商品的图片地址
export const reqSpuImageList = (spuId: number) => request.get(API.IMAGE_URL + spuId)
// 获取某一个已有的SPU拥有多少个销售属性
export const reqSpuHasSaleAttr = (spuId: number) => request.get(API.SPUHASSALEATTR_URL + spuId)
// 获取全部的销售属性
export const reqAllSaleAttr = () => request.get(API.ALLSALEATTR_URL)
// 添加一个新的SPU
// 更新已有的SPU
// data:即为新增的SPU|已有的SPU
export const reqAddOrUpdateSpu = (data: SpuData) => {
// 如果SPU对象拥有ID,更新已有的SPU
if (data.id) {
return request.post(API.UPDATESPU_URL, data)
} else {
return request.post(API.ADDSPU_URL, data)
}
}
//添加SKU的请求方法
export const reqAddSku = (data: SkuData) => request.post(API.ADDSKU_URL, data)
//获取SKU数据
export const reqSkuList = (spuId: number | string) => request.get(API.SKUINFO_URL + spuId)
//删除已有的SPU
export const reqRemoveSpu = (spuId: number | string) => request.delete(API.REMOVESPU_URL + spuId)
② 数据ts类型定义
src/api/product/spu/type.ts
// 服务器全部接口返回的数据类型
export interface ResponeData {
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 HasSpuResponeData extends ResponeData {
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 ResponeData {
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 ResponeData {
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 ResponeData {
data: SaleAttr[]
}
// 已有的全部SPU的返回数据ts类型
export interface HasSaleAttr {
id: number
name: string
}
export interface HasSaleAttrResponseData extends ResponeData {
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 ResponeData {
data: SkuData[]
}
SPU模块包括:
- Category组件
- table表格展示数据
- spuForm组件(即spu的增删改模块)
- skuForm组件(即sku的添加删除模块)
src/views/product/spu/index.vue
添加SPU
src/views/product/spu/spuForm.vue
添加销售属性
{{ item.saleAttrValueName }}
保存
取消
src/views/product/spu/skuForm.vue
设置默认
保存
取消
① 接口定义
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 reqHasSku = (page: number, limit: number) =>
request.get(API.SKU_URL + `${page}/${limit}`)
// 已有商品上架请求
export const reqSaleSku = (skuId: number) =>
request.get(API.SALE_URL + skuId)
// 下架的请求
export const reqCancelSaleSku = (skuId: number) =>
request.get(API.CANCELSALE_URL + skuId)
//获取商品详情的接口
export const reqSkuInfo = (skuId: number) =>
request.get(API.SKUINFO_URL + skuId)
//删除某一个已有的商品
export const reqRemoveSku = (skuId: number) =>
request.delete(API.DELETESKU_URL + skuId)
② 数据ts类型定义
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
}
src/views/product/sku/index.vue
查看商品详情
名称
{{ skuInfo.skuName }}
描述
{{ skuInfo.skuDesc }}
价格
{{ skuInfo.price }}
平台属性
{{ item.valueName }}
销售属性
{{ item.saleAttrValueName }}
商品图片