【Vue3+Ts项目】硅谷甄选 — 品牌管理+平台属性管理+SPU管理+SKU管理
一、品牌管理模块
1.1 静态模块搭建
使用到element-plus的card、button、table、pagination等组件:src/views/product/trademark/index.vue
<template><el-card><!-- 卡片顶部添加品牌按钮 --><el-button type="primary" size="default" icon="Plus">添加品牌</el-button><!-- 表格组件:用于展示已有的平台数据 --><!-- table---border:可以设置表格纵向是否有边框table-column---label:设置列的标题---width:设置列的宽度---align:设置列的对齐方式(left、center、right)--><el-table style="margin: 10px 0;" border><el-table-column label="序号" width="80px" align="center"></el-table-column><el-table-column label="品牌名称"></el-table-column><el-table-column label="品牌LOGO"></el-table-column><el-table-column label="品牌操作"></el-table-column></el-table><!-- 分页器组件paginationv-model:current-page:设置分页器当前页码v-model:page-size:设置每一个展示数据条数page-sizes:用于设置下拉菜单数据background:设置分页器按钮的背景颜色layout:可以设置分页器六个子组件布局调整--><el-pagination v-model:current-page="pageNo" v-model:page-size="limit" :page-sizes="[3, 5, 7, 9]":background="true" layout="prev, pager, next, jumper,->,sizes,total":total="400"/></el-card>
</template><script setup lang="ts">
// 引入组合式API函数ref
import {ref} from 'vue'
// 当前页码
let pageNo = ref<number>(1)
// 每一页展示多少条数据
let limit = ref<number>(3)
</script><style scoped></style>
1.2 品牌管理模块数据展示
1.2.1 书写trademark接口文件
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<any, TrademarkResponeData>(API.TRADEMARK_URL + `${page}/${limit}`)
1.2.2 接口数据ts类型定义
src/api/product/trademark/type.ts
export interface ResponeData {code: numbermessage: stringok: boolean
}// 已有的品牌的ts数据类型
export interface Trademark {id: numbertmName: stringlogoUrl: string
}// 包含全部品牌数据的ts类型
export type Records = Trademark[]// 获取的已有全部品牌的数据ts类型
export interface TrademarkResponeData extends ResponeData {data: {records: Recordstotal: numbersize: numbercurrent: numberorders: []optimizeCountSql: booleanhitCount: booleancountId: nullmaxLimit: nullsearchCount: booleanpages: number}
}
1.2.3 动态获取数据并展示
src/views/product/trademark/index.vue
table-column:默认展示数据用div,通过prop属性展示数据;如果需要自定义列的内容,可以使用插槽#来展示内容。
<template>
......
<el-table style="margin: 10px 0;" border :data="trademarkArr"><el-table-column label="序号" width="80px" align="center" type="index"></el-table-column><!-- table-column:默认展示数据用div --><el-table-column label="品牌名称" prop="tmName"></el-table-column><el-table-column label="品牌LOGO"><template #="{ row }"><img :src="row.logoUrl" style="width: 100px;height: 100px;"></template></el-table-column><el-table-column label="品牌操作"><el-button type="warning" size="small" icon="Edit"></el-button><el-button type="danger" size="small" icon="Delete"></el-button></el-table-column>
</el-table>
<el-pagination v-model:current-page="pageNo" v-model:page-size="limit" :page-sizes="[3, 5, 7, 9]" :background="true"layout="prev, pager, next, jumper,->,sizes,total" :total="total" />
......
</template><script setup lang="ts">
// 引入组合式API函数ref
import { ref, onMounted } from 'vue'
import { reqHasTrademark } from '@/api/product/trademark'
import type { Records, TrademarkResponeData } from '@/api/product/trademark/type'
// 当前页码
let pageNo = ref<number>(1)
// 每一页展示多少条数据
let limit = ref<number>(3)
// 存储已有品牌数据总数
let total = ref<number>(0)
// 存储已有品牌的数据
let trademarkArr = ref<Records>([])
// 获取已有品牌的接口封装为一个函数:在任何情况下想获取数据,调用函数即可
const getHasTrademark = async () => {let result: TrademarkResponeData = await reqHasTrademark(pageNo.value, limit.value)if (result.code === 200) {// 存储已有品牌总个数total.value = result.data.totaltrademarkArr.value = result.data.records}
}
// 组件挂载完毕的钩子---发一次请求,获取第一页,一页三个已有品牌数据
onMounted(() => {getHasTrademark()
})
</script>
1.3 品牌管理分页展示数据
给pagination组件标签添加current-change、size-change事件,书写对应方法,在当前页码和下拉菜单(每页展示的数据条数)发生变化时触发getHasTrademark回调,重新获取数据进行展示。
<el-pagination @size-change="sizeChange" @current-change="getHasTrademark" v-model:current-page="pageNo"v-model:page-size="limit" :page-sizes="[3, 5, 7, 9]" :background="true"layout="prev, pager, next, jumper,->,sizes,total" :total="total" />// 分页器当前页码发生变化的时候触发
// 对于当前页码发生变化自定义事件,组件pagination父组件回传了数据(当前的页码)
// const changePageNo = () =>{
// // 当前页码发生变化的时候再次发送请求获取对应已有品牌数据展示
// getHasTrademark()
// }// 当下拉菜单发生变化的时候触发此方法
// 这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {// 当前每一页的数据发生变化的时候,当前页码归1getHasTrademark()
}
1.4 对话框dialog(新增|修改品牌)
1.4.1 定义新增|修改接口
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<any, any>(API.UPDATETRADEMARK_URL, data)} else {// 新增品牌return request.post<any, any>(API.ADDTRADEMARK_URL, data)}
}
1.4.2 新增|修改业务逻辑实现
src/views/product/trademark/index.vue
图片大小计算:返回的rawFile.size是字节,1024字节 = 1K ,1024K = 1M, 1024M = 1G,
1024G = 1T
PS:表单校验步骤及说明可参考该文章 2.5:【Vue3+Ts项目】硅谷甄选 — 路由配置+登录模块+layout组件+路由鉴权-CSDN博客
......<!-- 卡片顶部添加品牌按钮 -->
<el-button type="primary" size="default" icon="Plus" @click="addTrademark">添加品牌</el-button>......<el-table-column label="品牌操作"><template #="{ row }"><el-button type="warning" size="small" icon="Edit" @click="updateTrademark(row)"></el-button><el-button type="danger" size="small" icon="Delete"></el-button></template>
</el-table-column>......<!-- 对话框组件:在添加品牌与修改已有品牌的业务时使用的结构 --><el-dialog v-model="dialogFormVisible" :title="trademarkParams.id ? '修改品牌' : '添加品牌'"><el-form style="width: 80%;" :model="trademarkParams" :rules="rules" ref="formRef"><el-form-item label="品牌名称" label-width="80px" prop="tmName"><el-input autocomplete="请您输入品牌名称" v-model="trademarkParams.tmName" /></el-form-item><el-form-item label="品牌LOGO" label-width="80px" prop="logoUrl"><el-upload class="avatar-uploader" action="api/admin/product/fileUpload" :show-file-list="false":on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"><img v-if="trademarkParams.logoUrl" :src="trademarkParams.logoUrl" class="avatar" /><el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon></el-upload></el-form-item></el-form><!-- 具名插槽:footer --><template #footer><el-button type="primary" size="default" @click="cancel">取消</el-button><el-button type="primary" size="default" @click="confirm">确认</el-button></template></el-dialog>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<boolean>(false)
// 定义收集新增品牌数据
let trademarkParams = reactive<Trademark>({tmName: '',logoUrl: ''
})
// 获取el-form组件实例
let formRef = ref()......// 添加品牌按钮的回调
const addTrademark = () => {//对话框显示dialogFormVisible.value = true// 清空收集数据trademarkParams.id = 0trademarkParams.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 4Mif (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 }]
}
1.5 品牌管理删除业务
使用element-plus气泡确认框( Popconfirm)来弹出删除提示。
src/views/product/trademark/index.vue
<el-popconfirm :title="`您确定要删除${row.tmName}吗?`" width="250px" icon="Delete" @confirm='removeTradeMark(row.id)'><template #reference><el-button type="danger" size="small" icon="Delete"></el-button></template>
</el-popconfirm>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: '删除品牌失败'})}
}
二、属性管理模块
2.1 静态模块搭建
三级分类在后续SPU管理模块也会使用到,所以将三级分类封装成一个全局组件更方便使用。
2.1.1 分类全局组件
使用el-select下拉菜单实现三级分类全局组件的静态搭建:src/components/Category/index.vue
<template><el-card><el-form :inline="true"><el-form-item label="一级分类"><el-select><el-option label="北京"></el-option><el-option label="上海"></el-option><el-option label="广州"></el-option><el-option label="深圳"></el-option></el-select></el-form-item><el-form-item label="二级分类"><el-select><el-option label="北京"></el-option><el-option label="上海"></el-option><el-option label="广州"></el-option><el-option label="深圳"></el-option></el-select></el-form-item><el-form-item label="三级分类"><el-select><el-option label="北京"></el-option><el-option label="上海"></el-option><el-option label="广州"></el-option><el-option label="深圳"></el-option></el-select></el-form-item></el-form></el-card>
</template><script setup lang="ts"></script><style scoped></style>
注册全局组件:src/components/index.ts
import Category from './Category/index.vue'// 全局组件对象
const allGlobalComponents: any = {Category,
}
PS:封装全局组件具体的实现可参考此文章:vue3---自定义插件注册全局对象-CSDN博客
2.1.2 属性管理组件
引入三级分类全局组件+使用el-table展示属性相关数据,实现属性管理组件的静态搭建:rc/views/product/attr/index.vue
<template><!-- 三级分类全局组件 --><Category /><el-card style="margin: 10px 0;"><el-button type="primary" size="default" icon="Plus">添加属性</el-button><el-table border style="margin: 10px 0;"><el-table-column label="序号" type="index" align="center" width="80px"></el-table-column><el-table-column label="属性名称" width="120px"></el-table-column><el-table-column label="属性值名称"></el-table-column><el-table-column label="操作" width="120px"></el-table-column></el-table></el-card>
</template><script setup lang="ts"></script><style scoped></style>
2.2 分类全局组件(Category)
分类全局组件挂载时获取一级分类,将获取到的一级分类数据和ID存储在仓库中(方便父组件使用分类ID获取属性相关数据), 通过一级分类的ID获取二级分类,二级分类的ID获取三级分类。
2.2.1 接口
① 接口定义
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<any, CategoryResponseData>(API.C1_URL)
// 获取二级分类的接口方法
export const reqC2 = (category1: number | string) => request.get<any, CategoryResponseData>(API.C2_URL + category1)
// 获取三级分类的接口方法
export const reqC3 = (category2: number | string) => request.get<any, CategoryResponseData>(API.C3_URL + category2)
② 数据ts类型定义
src/api/product/attr/type.ts
// 分类相关的数据ts类型
export interface ResponseData {code: numbermessage: stringok: boolean
}// 分类ts类型
export interface CategoryObj {id: number | stringname: stringcategory1Id?: numbercategory2Id?: number
}// 相应的分类接口返回数据类型
export interface CategoryResponseData extends ResponseData {data: CategoryObj[]
}
2.2.2 小仓库
① 创建小仓库
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: [],// 存储一级分类的IDc1Id: '',// 存储对应一级分类下二级分类的数据c2Arr: [],// 存储二级分类的IDc2Id: '',// 存储三级分类的数据c3Arr: [],// 存储三级分类的IDc3Id: ''}},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 | numberc1Arr: CategoryObj[]c2Arr: CategoryObj[]c2Id: string | numberc3Arr: CategoryObj[]c3Id: string | number
}
2.2.3 业务实现
src/components/Category/index.vue
<template><el-card><el-form :inline="true"><el-form-item label="一级分类"><!-- change:选中值发生变化时触发 --><el-select :disabled="scene === 1 ? true : false" v-model="categoryStore.c1Id" @change="handler"><!-- label:即为展示数据 value:即为select下拉菜单收集的数据 --><el-option v-for="(c1, index) in categoryStore.c1Arr" :key="c1.id" :label="c1.name":value="c1.id"></el-option></el-select></el-form-item><el-form-item label="二级分类"><el-select :disabled="scene === 1 ? true : false" v-model="categoryStore.c2Id" @change="handler1"><el-option v-for="(c2, index) in categoryStore.c2Arr" :key="c2.id" :label="c2.name":value="c2.id"></el-option></el-select></el-form-item><el-form-item label="三级分类"><el-select :disabled="scene === 1 ? true : false" v-model="categoryStore.c3Id"><el-option v-for="(c3, index) in categoryStore.c3Arr" :key="c3.id" :label="c3.name":value="c3.id"></el-option></el-select></el-form-item></el-form></el-card>
</template><script setup lang="ts">
// 引入组件挂载完毕方法
import { onMounted } from 'vue';
// 引入分类相关的仓库
import useCategoryStore from '@/store/modules/category'
let categoryStore = useCategoryStore()
// 分类全局组件挂载完毕,通知仓库发请求获取一级分类的数据
onMounted(() => {getC1()
})// 通知仓库获取一级分类的方法
const getC1 = () => {// 通知分类仓库发请求获取一级分类的数据categoryStore.getC1()
}// 此方法即为一级分类下拉菜单的change事件(选中值的时候会触发,保证一级分类ID有了)
const handler = () => {// 需要将二级、三级分类的数据清空categoryStore.c2Id = ''categoryStore.c3Arr = []categoryStore.c3Id = ''// 通知仓库获取二级分类的数据categoryStore.getC2()
}// 此方法即为二级分类下拉菜单的change事件(选中值的时候会触发,保证二级分类ID有了)
const handler1 = () => {// 清理三级分类的数据categoryStore.c3Id = ''categoryStore.getC3()
}// 接收父组件传递过来的scene
defineProps(['scene'])
</script><style scoped></style>
PS:当父组件传递scene为1时,下拉菜单select禁用,scene为0时,下拉菜单select正常使用。(Category中select组件需加上 :disabled="scene === 1 ? true : false")
2.3 属性管理主组件(attr)
提升开发效率的小Tips①:解析格式化JSON的网站(方便查看数据的结构):JSON在线解析及格式化验证 - JSON.cn
2.3.1 接口
① 接口定义
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<any, AttrResponeData>(API.ATTR_URL + `${category1Id}/${category2Id}/${category3Id}`)// 新增或修改已有属性的接口
export const reqAddOrUpdateAttr = (data: Attr) => request.post<any, any>(API.ADDORUPDATEATTR_URL, data)//删除某一个已有的属性业务
export const reqRemoveAttr = (attrId: number) => request.delete<any, any>(API.DELETEATTR_URL + attrId)
② 数据ts类型定义
src/api/product/attr/type.ts
// 属性与属性值的ts类型// 属性值对象的ts类型
export interface AttrValue {id?: numbervalueName: stringattrId?: numberflag: boolean
}// 存储每一个属性值的数组类型
export type AttrValueList = AttrValue[]// 属性对象的ts类型
export interface Attr {id?: numberattrName: stringcategoryId: number | stringcategoryLevel: numberattrValueList: AttrValueList
}// 存储每一个属性对象的数组ts类型
export type AttrList = Attr[]// 属性接口返回的数据ts类型
export interface AttrResponeData extends ResponseData {data: AttrList
}
2.3.2 业务实现
src/views/product/attr/index.vue
属性管理业务包括:展示数据、添加/修改数据、删除数据。
- 数据展示:通过三级分类下拉菜单存储的分类ID请求属性与属性值数据,使用el-table进行展示。
- 添加/修改数据:通过scene作为切换标识,进行数据展示与添加/修改数据页面切换,然后收集属性与属性值数据,发请求进行保存。
- 删除数据:使用el-popconfirm进行删除确认发请求。
<template><!-- 三级分类全局组件 --><Category :scene="scene" /><el-card style="margin: 10px 0;"><div v-show="scene === 0"><!-- 通过是否有三级分类ID来判断按钮是否禁用,没有ID:true,有:false --><el-button @click="addAttr" type="primary" size="default" icon="Plus":disabled="categoryStore.c3Id ? false : true">添加属性</el-button><el-table border style="margin: 10px 0;" :data="attrArr"><el-table-column label="序号" type="index" align="center" width="80px"></el-table-column><el-table-column label="属性名称" width="120px" prop="attrName"></el-table-column><el-table-column label="属性值名称"><template #="{ row }"><el-tag style="margin: 5px;" v-for="(item, index) in row.attrValueList" :key="item.id">{{ item.valueName}}</el-tag></template></el-table-column><el-table-column label="操作" width="120px"><!-- row:已有属性对象 --><template #="{ row }"><el-button type="warning" size="small" icon="Edit" @click="updateAttr(row)"></el-button><el-popconfirm :title="`你确定删除${row.attrName}?`" width="200px" @confirm="deleteAttr(row.id)"><template #reference><el-button type="danger" size="small" icon="Delete"></el-button></template></el-popconfirm></template></el-table-column></el-table></div><div v-show="scene === 1"><!-- 展示添加属性与修改数据的结构 --><el-form :inline="true"><el-form-item label="属性名称"><el-input placeholder="请您输入属性名称" v-model="attrParams.attrName"></el-input></el-form-item></el-form><el-button :disabled="attrParams.attrName ? false : true" type="primary" size="default" icon="Plus"@click="addAttrValue">添加属性值</el-button><el-button size="default" @click="cancel">取消</el-button><el-table border style="margin: 10px 0;" :data="attrParams.attrValueList"><el-table-column label="序号" type="index" align="center" width="80px"></el-table-column><el-table-column label="属性值名称"><!-- row:即为当前属性值对象 --><template #="{ row, $index }"><el-input :ref="(vc: any) => inputArr[$index] = vc" v-if="row.flag" size="small" @blur="toLook(row, $index)"placeholder="请您输入属性值名称" v-model="row.valueName"></el-input><div v-else @click="toEdit(row, $index)">{{ row.valueName }}</div></template></el-table-column><el-table-column label="属性值操作"><template #="{ $index }"><el-button type="danger" size="small" icon="Delete"@click="attrParams.attrValueList.splice($index, 1)"></el-button></template></el-table-column></el-table><el-button type="primary" size="default" @click="save":disabled="attrParams.attrValueList.length > 0 ? false : true">保存</el-button><el-button size="default" @click="cancel">取消</el-button></div></el-card>
</template><script setup lang="ts">
// 组合式API函数
import { nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
// 引入获取已有属性与属性值接口
import { reqAttr, reqAddOrUpdateAttr, reqRemoveAttr } from '@/api/product/attr'
import type { AttrResponeData, Attr, AttrValue } from '@/api/product/attr/type'
// 获取分类的仓库
import useCategoryStore from '@/store/modules/category'
import { ElMessage } from 'element-plus';
let categoryStore = useCategoryStore()
// 存储已有的属性与属性值
let attrArr = ref<Attr[]>([])
//定义card组件内容切换变量
let scene = ref<number>(0);//scene=0,显示table,scene=1,展示添加与修改属性结构
// 收集新增的属性的数据
let attrParams = reactive<Attr>({attrName: '', //新增属性名称categoryId: '', // 三级分类的IDcategoryLevel: 3, // 代表的是三级分类attrValueList: [] //新增的属性值数组
})
// 准备一个数组:将来存储对应的组件实例el-input
let inputArr = ref<any>([])
// 监听仓库三级分类ID变化
watch(() => categoryStore.c3Id, () => {// 清空上一次查询的属性与属性值attrArr.value = []// 保证三级分类得有才能发请求if (!categoryStore.c3Id) return//获取分类的IDgetAttr()
})// 获取已有的属性与属性值的方法
const getAttr = async () => {const { c1Id, c2Id, c3Id } = categoryStore// 获取分类下的已有的属性与属性值let result: AttrResponeData = await reqAttr(c1Id, c2Id, c3Id)if (result.code === 200) {attrArr.value = result.data}
}//添加属性按钮的回调
const addAttr = () => {// 每一次点击的时候,先清空一下数据再收集数据、Object.assign(attrParams, {attrName: '',categoryId: categoryStore.c3Id,categoryLevel: 3,attrValueList: []})//切换为添加与修改属性的结构scene.value = 1
}
//table表格修改已有属性按钮的回调
const updateAttr = (row: Attr) => {//切换为添加与修改属性的结构scene.value = 1//将已有的属性对象赋值给attrParams对象即为//ES6->Object.assign进行对象的合并Object.assign(attrParams, JSON.parse(JSON.stringify(row)))
}
//取消按钮的回调
const cancel = () => {scene.value = 0
}// 添加属性值按钮的回调
const addAttrValue = () => {// 点击添加属性值按钮的时候,向数组添加一个属性值对象attrParams.attrValueList.push({valueName: '',flag: true})//获取最后el-input组件聚焦nextTick(() => {inputArr.value[attrParams.attrValueList.length - 1].focus()})
}// 保存按钮的回调
const save = async () => {// 发请求let result: any = await reqAddOrUpdateAttr(attrParams)// 添加属性|修改已有的属性已经成功if (result.code === 200) {// 切换场景scene.value = 0// 提示信息ElMessage({type: 'success',message: attrParams.id ? '修改成功' : '添加成功'})// 获取全部已有的属性与属性值getAttr()} else {ElMessage({type: 'error',message: attrParams.id ? '修改失败' : '添加失败'})}
}// 属性值表单失去焦点事件回调
const toLook = (row: AttrValue, $index: number) => {// 非法情况判断1if (row.valueName.trim() === '') {// 删除对应属性值为空的元素attrParams.attrValueList.splice($index, 1)// 提示信息ElMessage({type: 'error',message: '属性值不能为空'})return}// 非法情况2let repeat = attrParams.attrValueList.find(item => {// 切记把当前失去焦点属性值对象从当前数组扣除判断if (item !== row) {return item.valueName === row.valueName}})if (repeat) {// 将重复的属性值从数组当中删除attrParams.attrValueList.splice($index, 1)// 提示信息ElMessage({type: 'error',message: '属性值不能重复'})return}// 相应的属性值对象flag:变为false,展示divrow.flag = false
}// 属性值div点击事件回调
const toEdit = (row: AttrValue, $index: number) => {// 相应的属性值对象flag:变为true,展示inputrow.flag = true// nextTick:响应式数据发生变化,获取更新的DOM(组件实例)nextTick(() => {inputArr.value[$index].focus()})
}//删除某一个已有的属性方法回调
const deleteAttr = async (attrId: number) => {//发相应的删除已有的属性的请求let result: any = await reqRemoveAttr(attrId)//删除成功if (result.code === 200) {ElMessage({type: 'success',message: '删除成功'})//获取一次已有的属性与属性值getAttr()} else {ElMessage({type: 'error',message: '删除失败'})}
}//路由组件销毁的时候,把仓库分类相关的数据清空
onBeforeUnmount(() => {//清空仓库的数据categoryStore.$reset()
})
</script><style scoped></style>
Object.assign:Object.assign为浅拷贝,如果修改属性时,直接使用Object.assign(attrParams, row)将已有的属性对象赋值给attrParams对象,会出现修改了属性之后点击取消按钮返回列表时,属性还是修改了;可以使用JSON方法进行深拷贝Object.assign(attrParams, JSON.parse(JSON.stringify(row)))。(关于深拷贝和浅拷贝的区别可以参考文章:js中深拷贝和浅拷贝的理解,它们的区别是什么-CSDN博客)
三、SPU管理模块
3.1 SPU和SKU概念介绍
SPU:电商术语,代表的是一个标准化产品单元。(类)
SPU组成:产品品牌名称+描述+产品图片介绍+销售属性【整个项目销售属性一共三个:颜色、版本、尺码】
例如,华为公司的品牌名称是华为,华为就是一个产品单元。
SKU:库存最小单位。(实例)
3.2 SPU实现
3.2.1 接口
① 接口定义
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',// 追加一个新的SPUADDSPU_URL = '/admin/product/saveSpuInfo',// 更新已有的SPUUPDATESPU_URL = '/admin/product/updateSpuInfo',//追加一个新增的SKU地址ADDSKU_URL = '/admin/product/saveSkuInfo',//查看某一个已有的SPU下全部售卖的商品SKUINFO_URL = '/admin/product/findBySpuId/',//删除已有的SPUREMOVESPU_URL = '/admin/product/deleteSpu/',
}// 获取某一个三级分类下已有的SPU数据
export const reqHasSpu = (page: number, limit: number, category3Id: number | string) => request.get<any, HasSpuResponeData>(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,更新已有的SPUif (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)
② 数据ts类型定义
src/api/product/spu/type.ts
// 服务器全部接口返回的数据类型
export interface ResponeData {code: numbermessage: stringok: boolean
}// SPU数据的ts类型
export interface SpuData {category3Id: string | numberid?: numberspuName: stringtmId: number | stringdescription: stringspuImageList: null | SpuImg[]spuSaleAttrList: null | SaleAttr[]
}// 数组:元素都是已有SPU数据类型
export type Records = SpuData[]// 定义获取已有的SPU接口返回的数据ts类型
export interface HasSpuResponeData extends ResponeData {data: {records: Recordstotal: numbersize: numbercurrent: numbersearchCount: booleanpages: number}
}// 品牌数据的ts类型
export interface Trademark {id: numbertmName: stringlogoUrl: string
}// 品牌接口返回的数据ts类型
export interface AllTrademark extends ResponeData {data: Trademark[]
}// 商品图片的ts类型
export interface SpuImg {id?: numberimgName?: stringimgUrl?: stringcreateTime?: stringupdateTime?: stringspuId?: numbername?: stringurl?: string
}
// 已有的SPU照片墙数据的类型
export interface SpuHasImg extends ResponeData {data: SpuImg[]
}// 已有的销售属性值对象ts类型
export interface SaleAttrValue {id?: numbercreateTime?: nullupdateTime?: nullspuId?: numberbaseSaleAttrId: number | stringsaleAttrValueName: stringsaleAttrName?: stringisChecked?: null
}// 存储已有的销售属性值数组类型
export type SpuSaleAttrValueList = SaleAttrValue[]// 销售属性对象ts类型
export interface SaleAttr {id?: numbercreateTime?: nullupdateTime?: nullspuId?: numberbaseSaleAttrId: number | stringsaleAttrName: stringspuSaleAttrValueList: SpuSaleAttrValueListflag?: booleansaleAttrValue?: string
}// SPU已有的销售属性接口返回数据ts类型
export interface SaleAttrResponseData extends ResponeData {data: SaleAttr[]
}// 已有的全部SPU的返回数据ts类型
export interface HasSaleAttr {id: numbername: string
}export interface HasSaleAttrResponseData extends ResponeData {data: HasSaleAttr[]
}export interface Attr {attrId: number | string //平台属性的IDvalueId: number | string //属性值的ID
}export interface saleArr {saleAttrId: number | string //属性IDsaleAttrValueId: number | string //属性值的ID
}
export interface SkuData {category3Id: string | number //三级分类的IDspuId: string | number //已有的SPU的IDtmId: string | number //SPU品牌的IDskuName: 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[]
}
3.2.2 业务实现
SPU模块包括:
- Category组件
- table表格展示数据
- spuForm组件(即spu的增删改模块)
- skuForm组件(即sku的添加删除模块)
src/views/product/spu/index.vue
<template><div><!-- 三级分类 --><Category :scene="scene" /><el-card style="margin: 10px 0;"><!-- v-if|v-show:都可以实现显示与隐藏 --><div v-show="scene === 0"><el-button type="primary" size="default" icon="Plus" :disabled="!categoryStore.c3Id"@click="addSpu">添加SPU</el-button><!-- 展示已有SPU数据 --><el-table border style="margin: 10px 0;" :data="records"><el-table-column label="序号" type="index" align="center" width="80px" /><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="warning" size="small" icon="Edit" title="修改SPU" @click="upadteSpu(row)"></el-button><el-button type="info" 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="danger" 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="handleSizeChange" /></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 title="SKU列表" v-model="show"><el-table :data="skuArr" border><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 { onBeforeUnmount, ref, watch } from 'vue';
// 引入分类的仓库
import useCategoryStore from '@/store/modules/category';
import { HasSpuResponeData, Records, SkuData, SkuInfoData, SpuData } from '@/api/product/spu/type';
import { reqHasSpu, reqRemoveSpu, reqSkuList } from '@/api/product/spu';
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, () => {// 务必保证有三级分类的IDif (!categoryStore.c3Id) returngetHasSpu()
})// 此方法执行:可以获取某一个三级分类下全部的已有的SPU
const getHasSpu = async (pager = 1) => {// 修改当前页码pageNo.value = pagerlet result: HasSpuResponeData = await reqHasSpu(pageNo.value, pageSize.value, categoryStore.c3Id)if (result.code === 200) {records.value = result.data.recordstotal.value = result.data.total}
}// 分页器下拉菜单发生变化的时候触发
const handleSizeChange = () => {getHasSpu()
}// 添加新的SPU按钮的回调
const addSpu = () => {// 切换为场景1:添加与修改已有SPU结构->SpuFormscene.value = 1//点击添加SPU按钮,调用子组件的方法初始化数据spu.value.initAddSpu(categoryStore.c3Id)
}// 修改已有的SPU的按钮的回调
const upadteSpu = (row: SpuData) => {// 切换为场景1:添加与修改已有SPU结构->SpuFormscene.value = 1// 调用子组件实例方法获取完整已有的SPU数据spu.value.initHasSpuData(row)
}// 子组件SpuForm绑定自定义事件:目前是让子组件通知父组件切换场景为0
const changeScene = (obj: any) => {// 子组件SpuForm点击取消变为场景0:展示已有的SPUscene.value = obj.flagif (obj.params === 'update') {//更新留在当前页getHasSpu(pageNo.value)} else {//添加留在第一页getHasSpu()}
}// 添加sku按钮的回调
const addSku = (row: SpuData) => {//点击添加SKU按钮切换场景为2scene.value = 2;// 调用子组件的方法,初始化添加SKU的数据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>
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 placeholder="请你选中品牌" v-model="spuParams.tmId"><el-option v-for="item 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="请你输入描述" 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 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 style="margin: 10px 0;" border :data="saleAttr"><el-table-column label="序号" type="index" align="center" width="80px" /><el-table-column label="属性名" width="100px" prop="saleAttrName"></el-table-column><el-table-column label="属性值"><template #="{ row }"><el-tag @close="row.spuSaleAttrValueList.splice(index, 1)" style="margin:0px 5px"v-for="(item, index) in row.spuSaleAttrValueList" :key="item.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 v-else @click="toEdit(row)" type="success" size="small" icon="Plus"></el-button></template></el-table-column><el-table-column label="操作" width="100px"><template #="{ row, $index }"><el-button type="danger" 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 size="default" @click="cancel">取消</el-button></el-form-item></el-form>
</template><script setup lang="ts">
import { reqAddOrUpdateSpu, reqAllSaleAttr, reqAllTrademark, reqSpuHasSaleAttr, reqSpuImageList } from '@/api/product/spu';
import { AllTrademark, HasSaleAttr, HasSaleAttrResponseData, SaleAttr, SaleAttrResponseData, SaleAttrValue, SpuData, SpuHasImg, SpuImg, Trademark } from '@/api/product/spu/type';
import { ElMessage } from 'element-plus';
import { computed, ref } from 'vue';
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>('')
let spuParams = ref<SpuData>({category3Id: "",//收集三级分类的IDspuName: "",//SPU的名字description: "",//SPU的描述tmId: '',//品牌的IDspuImageList: [],spuSaleAttrList: [],
})
//将来收集还未选择的销售属性的ID与属性值的名字
let saleAttrIdAndValueName = ref<string>('')
// 子组件书写一个方法
const initHasSpuData = async (spu: SpuData) => {//存储已有的SPU对象,将来在模板中展示spuParams.value = spu// spu:即为父组件传递过来的已有的SPU对象【不完整】// 获取全部品牌的数据let result1: AllTrademark = await reqAllTrademark()// 获取某一个品牌旗下全部售卖商品的图片let result2: SpuHasImg = await reqSpuImageList((spu.id as number))// 获取已有的SPU销售属性的数据let result3: SaleAttrResponseData = await reqSpuHasSaleAttr((spu.id as number))// 获取整个项目全部SPU的销售属性let result4: HasSaleAttrResponseData = await reqAllSaleAttr()// 存储全部品牌的数据allTrademark.value = result1.data// SPU对应商品图片imgList.value = result2.data.map(item => {return {name: item.imgName,url: item.imgUrl}})// 存储已有的SPU的销售属性saleAttr.value = result3.data// 存储全部的销售属性allSaleAttr.value = result4.data
}
//照片墙点击预览按钮的时候触发的钩子
const handlePictureCardPreview = (file: any) => {dialogImageUrl.value = file.url//对话框弹出来dialogVisible.value = true
}
//照片墙删除文件钩子
const handleRemove = (file: any) => {console.log(file);
}
//照片墙上传成功之前的钩子约束文件的大小与类型
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 = truerow.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,//图片的名字imaUrl: (item.response && item.response.data) || item.url}})//2:整理销售属性的数据spuParams.value.spuSaleAttrList = saleAttr.valuelet result = await reqAddOrUpdateSpu(spuParams.value)if (result.code === 200) {ElMessage({type: 'success',message: spuParams.value.id ? '更新成功' : '添加成功'})$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: "",//收集三级分类的IDspuName: "",//SPU的名字description: "",//SPU的描述tmId: '',//品牌的IDspuImageList: [],spuSaleAttrList: [],})// 清空照片imgList.value = []// 清空销售属性saleAttr.value = []saleAttrIdAndValueName.value = ''spuParams.value.category3Id = c3Id//获取全部品牌的数据let result: AllTrademark = await reqAllTrademark()let result1: HasSaleAttrResponseData = await reqAllSaleAttr()// 存储数据allTrademark.value = result.dataallSaleAttr.value = result1.data
}
// 对外暴露
defineExpose({ initHasSpuData, initAddSpu })</script><style scoped></style>
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 v-for="(item, index) in saleArr" :key="item.id" :label="item.saleAttrName"><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="warning" 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 size="default" @click="cancel">取消</el-button></el-form-item></el-form>
</template><script setup lang="ts">
import { reqAttr } from '@/api/product/attr'
import { reqSpuImageList, reqSpuHasSaleAttr, reqAddSku } from '@/api/product/spu'
import { SkuData } from '@/api/product/spu/type';
import { ElMessage } from 'element-plus';
import { reactive, ref } from 'vue';
//自定义事件的方法
let $emit = defineEmits(['changeScene'])
//平台属性
const attrArr = ref<any>([])
//销售属性
const saleArr = ref<any>([])
//照片的数据
const imgArr = ref<any>([])
// 获取table组件实例
const table = ref<any>()
//收集SKU的参数
let skuParams = reactive<SkuData>({//父组件传递过来的数据category3Id: "",//三级分类的IDspuId: "",//已有的SPU的IDtmId: "",//SPU品牌的ID//v-model收集skuName: "",//sku名字price: "",//sku价格weight: "",//sku重量skuDesc: "",//sku的描述skuAttrValueList: [//平台属性的收集],skuSaleAttrValueList: [//销售属性],skuDefaultImg: "",//sku图片地址
})
const initSkuData = async (c1Id: string | number, c2Id: string | number, spu: any) => {//收集数据skuParams.category3Id = spu.category3IdskuParams.spuId = spu.idskuParams.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
}
//对外暴露方法
defineExpose({ initSkuData })
// 取消按钮的回调
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
}//保存按钮的方法
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失败'})}
}
</script><style scoped></style>
四、SKU管理模块
4.1 SKU实现
4.1.1 接口
① 接口定义
src/api/product/sku/index.ts
// SKU模块接口管理
import request from '@/utils/request'
import type { SkuResponseData, SkuInfoData } from './type'
// 枚举地址
enum API {// 获取已有的商品的数据-SKUSKU_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<any, SkuResponseData>(API.SKU_URL + `${page}/${limit}`)
// 已有商品上架请求
export const reqSaleSku = (skuId: number) =>request.get<any, any>(API.SALE_URL + skuId)
// 下架的请求
export const reqCancelSaleSku = (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)
② 数据ts类型定义
src/api/product/sku/type.ts
export interface ResponseData {code: numbermessage: stringok: boolean
}
//定义SKU对象的ts类型
export interface Attr {id?: numberattrId: number | string //平台属性的IDvalueId: number | string //属性值的ID
}
export interface saleArr {id?: numbersaleAttrId: number | string //属性IDsaleAttrValueId: number | string //属性值的ID
}
export interface SkuData {category3Id?: string | number //三级分类的IDspuId?: string | number //已有的SPU的IDtmId?: string | number //SPU品牌的IDskuName?: 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: numbersize: numbercurrent: numberorders: []optimizeCountSql: booleanhitCount: booleancountId: nullmaxLimit: nullsearchCount: booleanpages: number}
}//获取SKU商品详情接口的ts类型
export interface SkuInfoData extends ResponseData {data: SkuData
}
4.1.2 业务实现
src/views/product/sku/index.vue
<template><el-card><el-table border style="margin: 10px" :data="skuAttr"><el-table-columnlabel="序号"type="index"align="center"width="80px"></el-table-column><el-table-columnlabel="名称"show-overflow-tooltipwidth="150px"prop="skuName"></el-table-column><el-table-columnlabel="描述"show-overflow-tooltipwidth="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-columnlabel="重量"width="150px"prop="weight"></el-table-column><el-table-columnlabel="价格"width="150px"prop="price"></el-table-column><el-table-column label="操作" width="280px" fixed="right"><template #="{ row, $index }"><el-button:type="row.isSale === 1 ? 'info' : 'success'"size="small":icon="row.isSale === 1 ? 'Bottom' : 'Top'"@click="updateSale(row)"></el-button><el-buttontype="primary"size="small"icon="Edit"@click="updateSku"></el-button><el-buttontype="info"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="danger" size="small" icon="Delete"></el-button></template></el-popconfirm></template></el-table-column></el-table><el-paginationv-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 0"><el-col :span="6">名称</el-col><el-col :span="18">{{ skuInfo.skuName }}</el-col></el-row><el-row style="margin: 10px 0"><el-col :span="6">描述</el-col><el-col :span="18">{{ skuInfo.skuDesc }}</el-col></el-row><el-row style="margin: 10px 0"><el-col :span="6">价格</el-col><el-col :span="18">{{ skuInfo.price }}</el-col></el-row><el-row style="margin: 10px 0"><el-col :span="6">平台属性</el-col><el-col :span="18"><el-tagstyle="margin: 5px"type="danger"v-for="item in skuInfo.skuAttrValueList":key="item.id">{{ item.valueName }}</el-tag></el-col></el-row><el-row style="margin: 10px 0"><el-col :span="6">销售属性</el-col><el-col :span="18"><el-tagstyle="margin: 5px"type="success"v-for="item in skuInfo.skuSaleAttrValueList":key="item.id">{{ item.saleAttrValueName }}</el-tag></el-col></el-row><el-row style="margin: 10px 0"><el-col :span="6">商品图片</el-col><el-col :span="18"><el-carousel :interval="4000" type="card" height="200px"><el-carousel-itemv-for="item in skuInfo.skuImageList":key="item.id"><img :src="item.imgUrl" 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 {reqCancelSaleSku,reqHasSku,reqRemoveSku,reqSaleSku,reqSkuInfo,
} from '@/api/product/sku'
import { SkuData, SkuInfoData, SkuResponseData } from '@/api/product/sku/type'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
// 分页器当前页码
let pageNo = ref<number>(1)
// 每一页展示几条数据
let pageSize = ref<number>(10)
let total = ref<number>(0)
let skuAttr = ref<SkuData[]>([])
//控制抽屉显示与隐藏的字段
let drawer = ref<boolean>(false)
let skuInfo = ref<any>({})
// 组件挂载完毕
onMounted(() => {getHasSku()
})
const getHasSku = async (pager = 1) => {//当前分页器的页码pageNo.value = pagerlet result: SkuResponseData = await reqHasSku(pageNo.value, pageSize.value)if (result.code === 200) {total.value = result.data.totalskuAttr.value = result.data.records}
}
// 分页器下拉菜单发生变化触发
const handler = (pageSize: number) => {getHasSku()
}
// 商品的上架与下架的操作
const updateSale = async (row: SkuData) => {//如果当前商品的isSale==1,说明当前商品是上架的额状态->更新为下架//否则else情况与上面情况相反if (row.isSale === 1) {// 下架操作await reqCancelSaleSku(row.id as number)// 提示信息ElMessage({type: 'success',message: '下架成功',})} else {// 上架操作await reqSaleSku(row.id as number)// 提示信息ElMessage({type: 'success',message: '上架成功',})}//发请求获取当前更新完毕的全部已有的SKUgetHasSku(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)if (result.code === 200) {//存储已有的SKUskuInfo.value = result.data}
}
//删除某一个已有的商品
const removeSku = async (id: number) => {//删除某一个已有商品的情况let result: any = await reqRemoveSku(id)if (result.code === 200) {//提示信息ElMessage({type: 'success',message: '删除成功',})//获取已有全部商品getHasSku(pageNo.value > 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>
相关文章:

【Vue3+Ts项目】硅谷甄选 — 品牌管理+平台属性管理+SPU管理+SKU管理
一、品牌管理模块 1.1 静态模块搭建 使用到element-plus的card、button、table、pagination等组件:src/views/product/trademark/index.vue <template><el-card><!-- 卡片顶部添加品牌按钮 --><el-button type"primary" size&quo…...

计算机图形学流体模拟 blender 渲染脚本
做流体模拟的时候,想要复现别人的成果,但是别人的代码都是每帧输出 ply 格式的文件,渲染部分需要自己完成 看了一下,似乎用 blender 是最简单的,于是记录一下过程中用到的代码 Blender 版本 4.0 批量导入 ply 假设…...

二分图带权最大匹配-KM算法详解
文章目录 零、前言一、红娘再牵线二、二分图带权最大完备匹配2.1二分图带权最大匹配2.2概念2.3KM算法2.3.1交错树2.3.2顶标2.3.3相等子图2.3.4算法原理2.3.5算法实现 三、OJ练习3.1奔小康赚大钱3.2Ants 零、前言 关于二分图:二分图及染色法判定-CSDN博客 关于二分…...

Redis命令 - Sets命令组常用命令
Set集合,无序,一堆不重复值的组合。利用redis提供的set数据结构,可以存储一些集合性的数据。 使用场景:例如,实现如共同关注、共同喜好、二度好友等 1、SADD key member [member …] 向集合中添加一个或者多个成员 …...

DA14531-外设驱动篇-I2C通信应用
文章目录 1.I2C通信应用相关文件2.宏定义列表3.主要函数接口4.应用代码实例1.I2C通信应用相关文件 1)i2c.c和i2c.h(SDK文件) 2)app_I2cProtocol.c和app_I2cProtocol.h(用户应用文件) 2.宏定义列表 宏定义注解I2C_ADDRESSING_7B7-bit 地址I2C_ADDRESSING_10B10-bit 地址…...

Git仓库管理笔记
问题: hint: the same ref. If you want to integrate the remote changes, use Done 解决: 解决方法: 1、先使用pull命令: git pull --rebase origin master 2、再使用push命令: git push -u origin master...

[嵌入式软件][入门篇] 搭建在线仿真平台(STM32)
文章目录 一、注册平台二、创建首个项目三、硬件介绍 一、注册平台 进入官方,进行注册: 在线仿真地址 二、创建首个项目 ① 新建项目 ② 搭建一个电路 ③ 用STM32F103搭建一个简单电路 ④ 进入编码界面 三、硬件介绍 红框是必看文档ÿ…...

设置5台SSH互免的虚拟机服务器配置
搭建一套集群虚拟机,往往都需要互免设置,过程很简单,避免以后再搭建还得网上搜索,我直接将这一个步骤写成笔记,记录下来,方便后续查阅。 步骤如下—— 1、准备五台机器 服务器名字服务器IPhadoop1192.16…...

深信服技术认证“SCCA-C”划重点:交付和运维体系
为帮助大家更加系统化地学习云计算知识,高效通过云计算工程师认证,深信服特推出“SCCA-C认证备考秘笈”,共十期内容。“考试重点”内容框架,帮助大家快速get重点知识。 划重点来啦 *点击图片放大展示 深信服云计算认证ÿ…...

xlua源码分析(五) struct类型优化
xlua源码分析(五) struct类型优化 上一节我们分析了xlua是如何实现lua层访问C#值类型的,其中我们重点提到了xlua默认实现方式下,struct访问的效率问题。实际上,xlua还提供了两种优化的方式,可以大大提高str…...

iptables TEE模块测试小记
概述 因为公司项目需求,需要对服务器特定端口进行流量镜像,各种百度之后,发现TEE的模块,后来一番折腾,发现被转发的机器死活收不到数据,最后tcpdump一通了解到根源,博文记录,用以备…...

facebook广告怎么设置受众人群
在设置Facebook广告受众人群时,你可以遵循以下步骤: 打开广告创建工具,点击页面右上角的箭头并选择“创建广告”。选择广告目标,根据想要实现的目标创建广告。例如,想要让更多用户谈论你的主页和帖子,或者…...

MySQL夯实之路-MVCC机制深入浅出
多版本并发控制(MVCC,multiversion concurrency control) MVCC用更加灵活的方式处理并发,实现了读不加锁,读写不冲突。保证了事务的隔离性(可重复读),避免了不可重复读问题。 数据…...

Java线上问题堆栈排查分析
最近线上出现类似内存溢出问题,需要排查具体原因,记录过程,方便备查。 一、数据抓取 在启动参数中添加参数,可参照以下设置。 参数的作用是在程序发生内存溢出 OutOfMemory 时打印日志,dump下来,方便用工…...

C语言代码 计算1!+2!+3!+4!+5!+6!+7!+8!+9!+10!
计算1!2!3!4!5!6!7!8!9!10! 代码示例: #include <stdio.h> int main() {int i 0;int n 0;int ret 1;int sum 0;for (n 1; n < 10; n){ret 1;for (i 1; i < n; i){ret ret * i;}sum sum ret;}printf("%d\n", sum);return 0; } 运…...

【RTOS】快速体验FreeRTOS所有常用API(4)队列
目录 四、队列2.1 概念2.2 创建队列2.3 写队列2.4 读队列2.5 队列集(可跳过) 四、队列 该部分在上份代码基础上修改得来,代码下载链接: https://wwzr.lanzout.com/iBNAS1l75bvc 密码:7xy2 该代码尽量做到最简,不添加多…...

【开题报告】基于SpringBoot的美食制作学习网站的设计设计与实现
1.选题背景 随着人们生活水平的提高,对美食的追求也越来越高。越来越多的人希望能够在家里制作出各种美味的菜肴。然而,对于许多人来说,缺乏专业的指导和实践经验是一个挑战。另外,互联网的普及与发展,为人们提供了更…...

Rosalind Java|Speeding Up Motif Finding
Rosalind编程问题之计算错误矩阵(failure array)输出前后缀检索匹配。 Speeding Up Motif Finding Problem: A prefix of a length n string s is a substring s[1:j]; a suffix of s is a substring s[k:n]. The failure array of s is a…...

打印的前后顺序
面试题经常会有 <script>console.log(1)setTimeout(function(){console.log(2)})console.log(3)let pnew Promise((resolve,reject) >{console.log(4)resloved(hhhhhh)})p.then(res >{console.log(res)console.log(5)},res >{console.log(7)})console.log(6)&l…...

Android Retrofit使用详情
一、 Retrofit是什么 Retrofit是Android用来接口请求的网络框架,内部是基于OkHttp实现的,retrofit负责接口请求的封装,retrofit可以直接将接口数据解析为Bean类、List集合等,直接简化了中间繁琐的数据解析过程 二、 Retrofit的简单…...

安全加密算法
常用加密算法 对称加密 加密和解密用到的密钥是相同的,这种加密方式加密速度非常快,适合经常发送数据的场合。缺点是密钥的传输比较麻烦。常用对称加密算法如下: DES:密钥长度8个字节,安全性不足,已被证明…...

软件测试|使用matplotlib绘制多种饼图
简介 Matplotlib是一个强大的数据可视化库,它允许我们创建各种类型的图表,包括饼图。饼图是一种用于显示数据分布的常见图表类型。在本文中,我们将介绍如何使用Matplotlib创建不同类型的饼图,并提供示例代码。 创建标准饼图 首…...

vue3-响应式基础之ref
声明响应式状态 ref() 在组合式 API 中,推荐使用 ref() 函数来声明响应式状态: ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回: import { ref } from vue const count ref(0)console.log(count) // { va…...

华为网络设备 通过路由器子接口 Dot1q终结子接口实现跨VLAN通信
(二层交换机直接跳过三层交换价接入路由器时才使用该配置。推荐使用三层交换机建立VLANIF配置更简洁明了。如果VLAN较少可直接配置;路由器接口,一个物理接口一个VLAN) S1配置 vlan batch 2 to 3interface GigabitEthernet0/0/1port link-type trunkpor…...

代码随想录算法训练48 | 动态规划part09
今天就是打家劫舍的一天,这个系列不算难,大家可以一口气拿下。 198.打家劫舍 视频讲解:动态规划,偷不偷这个房间呢?| LeetCode:198.打家劫舍_哔哩哔哩_bilibili 代码随想录 213.打家劫舍II 视频讲解&am…...

2024最新适用于 Windows 、Mac 的最佳屏幕录制软件
屏幕录制软件可以帮助我们录制 PC 和MacBook的实时屏幕视频。如果您想为 优酷录制视频,或者您正在为您的公司制作基于视频的项目,并且需要捕获屏幕的实时视频录制,那么我们在此列出了 一 款适合您的 Windows 、Mac的 2024 年最佳屏幕录制软件…...

【Docker】概述与安装
🥳🥳Welcome Huihuis Code World ! !🥳🥳 接下来看看由辉辉所写的关于Docker的相关操作吧 目录 🥳🥳Welcome Huihuis Code World ! !🥳🥳 一. Docker的概述 1.Docker为什么出现 2…...

衡水学院新人真题百练2022(1-20)修订版
1 重要的话说三遍 分数 5 作者 陈越 单位 浙江大学 这道超级简单的题目没有任何输入。 你只需要把这句很重要的话 —— “I’m gonna WIN!”——连续输出三遍就可以了。 注意每遍占一行,除了每行的回车不能有任何多余字符。 #include<stdio.h> int…...

远程调用(OpenFeign)
远程调用 何为远程调用?例如:单体服务时,A模块的功能涉及到引用B模块的功能,那我们需要在A模块中注入B模块的相关服务类并调用其方法;那么同样的逻辑在微服务体系下,就会变成了A服务的功能需要调用B服务的功能,这就形成了服务间调用,也称为远程调用。 目前来说,微服务…...