挑战一周完成Vue3项目Day3: 品牌管理+平台属性管理+SPU管理+SKU管理

一、真实接口替换mock接口

(1)替换各个环境下的服务器地址( .env.development、.env.production、.env.test )

VITE_SERVE="http://sph-api.atguigu.cn"

(2) 配饰代理跨域:vite.config.ts(具体配置参数可参考官网:开发服务器选项 | Vite 官方中文文档)

export default defineConfig(({ command, mode }) => {// 获取各种环境下对应的变量let env = loadEnv(mode, process.cwd())return {......// 代理跨域server: {proxy: {[env.VITE_APP_BASE_API]: {// 获取数据的服务器地址设置target: env.VITE_SERVE,// 是否代理跨域changeOrigin: true,// 路径重写rewrite: (path) => path.replace(/^\/api/, ''),}}}}
})

(3)重新书写API接口文件及接口类型文件src/api/user/index.ts

// 统一管理项目用户相关的接口
import request from '@/utils/request'
import type {loginForm,loginResponseData,userResponseData,
} from './type'
// 项目用户相关的请求地址
enum API {LOGIN_URL = '/admin/acl/index/login',USERINFO_URL = '/admin/acl/index/info',LOGOUT_URL = '/admin/acl/index/logout',
}// 暴露请求函数
// 登录接口
export const reqLogin = (data: loginForm) =>request.post<any, loginResponseData>(API.LOGIN_URL, data)
// 获取用户信息
export const reqUserInfo = () =>request.get<any, userResponseData>(API.USERINFO_URL)
// 退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

(4)src/store/modules/user.ts

// 创建用户相关的小仓库
import { defineStore } from 'pinia'
// 接口
import { reqLogin,reqUserInfo,reqLogout } from '@/api/user'
// 引入路由(常量路由)
import { constantRoute } from '@/router/routes';
// 引入数据类型
// import type { loginForm, loginResponseData } from '@/api/user/type'
import type { UserState } from './types/type'
// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'//创建用户小仓库
let useUserStore = defineStore('User', {//小仓库存储数据地state: (): UserState => {return {token: GET_TOKEN(), //存储用户唯一标识,本地存储持久化tokenmenuRoutes: constantRoute, //仓库存储菜单需要的数组(路由)username: '',avatar: '',}},//异步|逻辑的地方actions: {// 用户登录的方法async userLogin(data:any) {// 登录请求let result:any = await reqLogin(data)console.log(result)//登录请求:成功200->token//登录请求:失败201->登录失败错误的信息if (result.code == 200) {//由于pinia|vuex存储数据其实利用js对象//pinia仓库存储一下tokenthis.token = result.data as string//本地存储持久化存储一份// localStorage.setItem('TOKEN', result.data.token as string)SET_TOKEN(result.data as string)// 能保证当前asnyc函数返回一个成功的promisereturn 'ok'} else {return Promise.reject(new Error(result.data))}},// 获取用户信息async userInfo() {// 获取用户信息进行存储仓库当中(用户头像、名字)let result:any = await reqUserInfo()// 如果获取信息成功,存储下用户信息if (result.code === 200) {this.username = result.data.namethis.avatar = result.data.avatar}else{return Promise.reject(new Error(result.message))}},// 退出登录async userLogout() {let result = await reqLogout();if (result.code === 200) {// 目前没有mock接口:退出登录接口(通知服务器本地用户唯一标识失败)this.token = ''this.username = ''this.avatar = ''REMOVE_TOKEN()return 'ok'}else{return Promise.reject(new Error(result.message))}},},getters: {},
})
//对外暴露获取小仓库方法
export default useUserStore

(5)src/layout/tabber/setting/index.vue  permission.ts

二、接口ts类型定义

(1) src/api/user/type.ts

// 定义用户相关数据的ts类型
// 用户登录接口携带参数的ts类型
export interface loginFormData {username: stringpassword: string
}// 定义全部接口返回数据都拥有的ts类型
export interface ResponseData {code: numbermessage: stringok: boolean
}// 定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {data: string
}// 定义获取用户信息返回的数据类型
export interface userInfoResponseData extends ResponseData {data: {routes: string[]buttons: string[]roles: string[]name: stringavatar: string}
}

 (2)src/api/user/index.ts

// 统一管理项目用户相关的接口
import request from '@/utils/request'
import type {loginFormData,loginResponseData,userInfoResponseData,
} from './type'
// 项目用户相关的请求地址
enum API {LOGIN_URL = '/admin/acl/index/login',USERINFO_URL = '/admin/acl/index/info',LOGOUT_URL = '/admin/acl/index/logout',
}// 暴露请求函数
// 登录接口
export const reqLogin = (data: loginFormData) =>request.post<any, loginResponseData>(API.LOGIN_URL, data)
// 获取用户信息
export const reqUserInfo = () =>request.get<any, userInfoResponseData>(API.USERINFO_URL)
// 退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

三、品牌管理模块

1.静态模块搭建

Pagination 分页 | Element Plus

表单组件table

        ---border:可以设置表格纵向是否有边框

        table-column

        ---label:设置列的标题

        ---width:设置列的宽度

        ---align:设置列的对齐方式(left、center、right)

分页器pagination

        v-model:current-page:设置分页器当前页码

        v-model:page-size:设置每一个展示数据条数

        page-sizes:用于设置下拉菜单数据

        background:设置分页器按钮的背景颜色

        layout:可以设置分页器六个子组件布局调整

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>

 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}`)

(2)接口数据ts类型定义src/api/product/trademark/type.ts

export interface ResponseData {code: numbermessage: stringok: boolean
}// 已有的品牌的ts数据类型
export interface Trademark {id?: numbertmName: stringlogoUrl: string
}// 包含全部品牌数据的ts类型
export type Records = Trademark[]// 获取的已有全部品牌的数据ts类型
export interface TrademarkResponeData extends ResponseData {data: {records: Recordstotal: numbersize: numbercurrent: numberorders: []optimizeCountSql: booleanhitCount: booleancountId: nullmaxLimit: nullsearchCount: booleanpages: number}
}

(3)动态获取数据并展示src/views/product/trademark/index.vue

table-column:默认展示数据用div,通过prop属性展示数据;如果需要自定义列的内容,可以使用插槽#来展示内容。

<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 :data="trademarkArr"><el-table-column label="序号" width="80px" align="center" type="index"></el-table-column><el-table-column label="品牌名称" prop="tmName"></el-table-column><!-- table-column默认展示数据用的是div --><el-table-column label="品牌LOGO"><template #="{ row }"><img :src="row.logoUrl" style="width: 100px;height: 100px;"></template></el-table-column><el-table-column label="品牌操作"><!-- <template #="{ row,$index }"> --><el-button type="primary" size="small" icon="Edit"></el-button><el-button type="primary" size="small" icon="Delete"></el-button><!-- </template> --></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="total" /></el-card>
</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><style scoped></style>

3、品牌管理分页数据展示

Pagination 分页 | Element Plus

给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" />const getHasTrademark = async (pager=1) => {// 当前页码pageNo.value=pager
.....
}// 分页器当前页码发生变化的时候触发
// 对于当前页码发生变化自定义事件,组件pagination父组件回传了数据(当前的页码)
// const changePageNo = () =>{
//   // 当前页码发生变化的时候再次发送请求获取对应已有品牌数据展示
//   getHasTrademark()
// }// 当下拉菜单发生变化的时候触发此方法
// 这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {// 当前每一页的数据发生变化的时候,当前页码归1getHasTrademark()
}

 4.对话框dialog

4.1静态对话框dialog

Upload 上传 | Element Plus

src/views/product/trademark/index.vue 

<!-- 对话框组件:在添加或修改品牌时的结构 --><el-dialog v-model="dialogFormVisible" title="修改"><el-form style="width: 80%;"><el-form-item label="品牌名称" label-width="80px"><el-input placeholder="请你输入品牌名称"></el-input></el-form-item><el-form-item label="品牌logo" label-width="80px"><el-upload class="avatar-uploader" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15":show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload"><img v-if="imageUrl" :src="imageUrl" class="avatar" /><el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon></el-upload></el-form-item></el-form><!-- 具名插槽 --><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>
...
// 控制对话框显示与隐藏
let dialogFormVisible = ref<boolean>(false)
....
// 添加按钮回调
const addTrademark = () => {// 对话框提示dialogFormVisible.value = true
}
// 修改按钮回调
const updateTrademark = () => {// 对话框提示dialogFormVisible.value = true
}
// 取消按钮
const cancel = () => {dialogFormVisible.value = false
}
// 确定按钮
const confirm = () => {dialogFormVisible.value = false
}<style scoped>
.avatar-uploader .avatar {width: 178px;height: 178px;display: block;
}
</style><style>
.avatar-uploader .el-upload {border: 1px dashed var(--el-border-color);border-radius: 6px;cursor: pointer;position: relative;overflow: hidden;transition: var(--el-transition-duration-fast);
}.avatar-uploader .el-upload:hover {border-color: var(--el-color-primary);
}.el-icon.avatar-uploader-icon {font-size: 28px;color: #8c939d;width: 178px;height: 178px;text-align: center;
}
</style>

4.2对话框接口业务实现

(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)}
}

(2)新增|修改业务逻辑实现

src/views/product/trademark/index.vue  

这一部分老师讲的很细,涉及到表单校验,删除修改添加功能的实现。需要认真听。

<template><el-card><!-- 卡片顶部添加品牌按钮 --><el-button type="primary" size="default" icon="Plus" @click="addTrademark">添加品牌</el-button><!-- 表格组件:用于展示已有的平台数据 --><!-- table---border:可以设置表格纵向是否有边框table-column---label:设置列的标题---width:设置列的宽度---align:设置列的对齐方式(left、center、right)--><el-table style="margin: 10px 0;" border :data="trademarkArr"><el-table-column label="序号" width="80px" align="center" type="index"></el-table-column><el-table-column label="品牌名称" prop="tmName"></el-table-column><!-- table-column默认展示数据用的是div --><el-table-column label="品牌LOGO"><template #="{ row }"><img :src="row.logoUrl" style="width: 100px;height: 100px;"></template></el-table-column><el-table-column label="品牌操作"><template #="{ row }"><el-button type="primary" size="small" icon="Edit" @click="updateTrademark(row)"></el-button><el-button type="primary" size="small" icon="Delete"></el-button></template></el-table-column></el-table><!-- 分页器组件paginationv-model:current-page:设置分页器当前页码v-model:page-size:设置每一个展示数据条数page-sizes:用于设置下拉菜单数据background:设置分页器按钮的背景颜色layout:可以设置分页器六个子组件布局调整--><el-pagination @current-change="getHasTrademark" @size-change="sizeChange" :pager-count="9"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" /></el-card><!-- 对话框组件:在添加或修改品牌时的结构 --><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="100px" prop="tmName"><el-input placeholder="请你输入品牌名称" v-model="trademarkParams.tmName"></el-input></el-form-item><el-form-item label="品牌logo" label-width="100px" 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><!-- 具名插槽 --><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>
</template><script setup lang="ts">
import { ElMessage, UploadProps } from 'element-plus'
// 引入组合式API函数ref
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 pageNo = ref<number>(1)
// 每一页展示多少条数据
let limit = ref<number>(3)
// 存储已有品牌数据总数
let total = ref<number>(0)
// 存储已有品牌的数据
let trademarkArr = ref<Records>([])
// 控制对话框显示与隐藏
let dialogFormVisible = ref<boolean>(false)
// 定义收集新增品牌数据
let trademarkParams = reactive<Trademark>({tmName: '',logoUrl: ''
})
let formRef = ref()
// 获取已有品牌的接口封装为一个函数:在任何情况下想获取数据,调用函数即可
const getHasTrademark = async (pager = 1) => {// 当前页码pageNo.value = pagerlet result: TrademarkResponeData = await reqHasTrademark(pageNo.value, limit.value)if (result.code === 200) {// 存储已有品牌总个数total.value = result.data.totaltrademarkArr.value = result.data.records}
}
// 组件挂载完毕的钩子---发一次请求,获取第一页,一页三个已有品牌数据
onMounted(() => {getHasTrademark()
});
// 分页器当前页码发生变化的时候触发
// 对于当前页码发生变化自定义事件,组件pagination父组件回传了数据(当前的页码)
// const changePageNo = () =>{
//   // 当前页码发生变化的时候再次发送请求获取对应已有品牌数据展示
//   getHasTrademark()
// }// 当下拉菜单发生变化的时候触发此方法
// 这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {// 当前每一页的数据发生变化的时候,当前页码归1// pageNo.value=1getHasTrademark()
}
// 添加按钮回调
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是当前已有的品牌
const updateTrademark = (row:Trademark) => {// 清空校验规则错误提示信息nextTick(() => {formRef.value.clearValidate('tmName')formRef.value.clearValidate('logoUrl')})// 对话框提示dialogFormVisible.value = true// es6合并语法Object.assign(trademarkParams, row)// 修改// trademarkParams.id=row.id// trademarkParams.tmName=row.tmName// trademarkParams.logoUrl=row.logoUrl
}
// 取消按钮
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) => {console.log(uploadFile);// response:即当前这次上传图片post请求服务器返回的数据// 收集上传图片的地址,添加一个新的品牌的时候带给服务器trademarkParams.logoUrl = response.data// 图片上传成功,清除掉对应图片校验错误提示信息formRef.value.clearValidate('logoUrl')
}
// 品牌名称自定义校验规则方法
const validatorTmName = (rule: any, value: any, callBack: any) => {// 是表单元素触发blur时候,会触发此方法console.log(rule);// 自定义校验规则if (value.trim().length >= 2) {callBack()} else {// 校验未通过返回的错误提示信息callBack(new Error('品牌名称位数大于等于两位'))}
}// 品牌LOGO图片的自定义校验规则方法
const validatorLogoUrl = (rule: any, value: any, callBack: any) => {console.log(rule);// 如果图片上传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 }]
}
</script><style scoped>
.avatar-uploader .avatar {width: 178px;height: 178px;display: block;
}
</style><style>
.avatar-uploader .el-upload {border: 1px dashed var(--el-border-color);border-radius: 6px;cursor: pointer;position: relative;overflow: hidden;transition: var(--el-transition-duration-fast);
}.avatar-uploader .el-upload:hover {border-color: var(--el-color-primary);
}.el-icon.avatar-uploader-icon {font-size: 28px;color: #8c939d;width: 178px;height: 178px;text-align: center;
}
</style>

5. 品牌管理删除业务

使用element-plus气泡确认框( Popconfirm)来弹出删除提示

(1)src/api/product/trademark/index.ts

//删除已有品牌DELETE_URL = '/admin/product/baseTrademark/remove/'
......
//删除某一个已有品牌的数据
export const reqDeleteTrademark = (id:number)=>request.delete<any,any>(API.DELETE_URL+id)

(2)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: '删除品牌失败'})}
}

四、属性管理模块

1.静态搭建

三级分类在后续SPU管理模块也会使用到,所以将三级分类封装成一个全局组件更方便使用。 

(1)分类全局组件

使用el-select下拉菜单实现三级分类全局组件的静态搭建:src/components/Category/index.vue

<template><el-card><el-form :inline="true"><el-form-item label="一级分类"><el-select style="width:150px;"><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 style="width:150px;"><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 style="width:150px;"><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>

 (2)注册全局组件:src/components/index.ts

import Category from './Category/index.vue'// 全局组件对象
const allGlobalComponents: any = {Category,
}

(3)属性管理组件,引入三级分类全局组件+使用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.分类全局组件(Category)

分类全局组件挂载时获取一级分类,将获取到的一级分类数据和ID存储在仓库中(方便父组件使用分类ID获取属性相关数据), 通过一级分类的ID获取二级分类,二级分类的ID获取三级分类。

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)

2.2 数据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.3创建小仓库

 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

2.4state数据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.5业务实现

src/components/Category/index.vue

<template><el-card><el-form :inline="true"><el-form-item label="一级分类"><el-select  v-model="categoryStore.c1Id" @change="handler" style="width:150px;"><!-- label:即为展示数据 value:即为select下拉菜单收集的数据 --><el-option v-for="(c1,) 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 v-model="categoryStore.c2Id" @change="handler1" style="width:150px;"><el-option v-for="(c2,) 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 v-model="categoryStore.c3Id" style="width:150px;"><el-option v-for="(c3,) 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()
}
</script><style scoped></style>

src/views/product/attr/index.vue 

<template><div>....<!-- :disabled如果没有c3id就禁用添加属性按钮 --><el-button type="primary" size="default" icon="Plus" :disabled="categoryStore.c3Id ? false : true">添加属性</el-button>......</el-card></div>
</template><script setup lang="ts">
// 获取分类的仓库
import useCategoryStore from '@/store/modules/category'
let categoryStore = useCategoryStore()
</script>

3、属性管理主组件(attr)

提升开发效率的小Tips①:解析格式化JSON的网站(方便查看数据的结构):JSON在线解析及格式化验证 - JSON.cn 

(1)接口定义 

src/api/product/attr/index.ts

import type { ......, AttrResponseData, 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)

 (2)数据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 AttrResponseData extends ResponseData {data: AttrList
}

(3)业务实现

属性管理业务包括:展示数据、添加/修改数据、删除数据。

数据展示:通过三级分类下拉菜单存储的分类ID请求属性与属性值数据,使用el-table进行展示。
添加/修改数据:通过scene作为切换标识,进行数据展示与添加/修改数据页面切换,然后收集属性与属性值数据,发请求进行保存。
删除数据:使用el-popconfirm进行删除确认发请求。

src/views/product/attr/index.vue

<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,) 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 { AttrResponseData, 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: AttrResponseData = 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>

 src/components/category/index.vue

<template><el-card><el-form :inline="true"><el-form-item label="一级分类"><el-select :disabled="scene == 0 ? false : true" v-model="categoryStore.c1Id" @change="handler" style="width:150px;"><!-- label:即为展示数据 value:即为select下拉菜单收集的数据 --><el-option v-for="(c1,) 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 == 0 ? false : true" v-model="categoryStore.c2Id" @change="handler1" style="width:150px;"><el-option v-for="(c2,) 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 == 0 ? false : true" v-model="categoryStore.c3Id" style="width:150px;"><el-option v-for="(c3,) 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>

五、SPU管理模块

SPU/SKU介绍

SPU:电商术语,代表的是一个标准化产品单元。(类)

SPU组成:产品品牌名称+描述+产品图片介绍+销售属性【整个项目销售属性一共三个:颜色、版本、尺码】

例如,华为公司的品牌名称是华为,华为就是一个产品单元。

SKU:库存最小单位。(实例)

1、SPU静态页面搭建

src/views/product/spu/index.vue 

<template><div><!-- 三级分类 --><Category :scene="scene"></Category><el-card style="margin:10px 0px"><el-button type="primary" size="default" icon="Plus">添加SPU</el-button><el-table style="margin:10px 0px" border><el-table-column label="序号" type="index" align="center" width="80px"></el-table-column><el-table-column label="名称"></el-table-column><el-table-column label="SPU描述"></el-table-column><el-table-column label="SPU操作"></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="400" /></el-card></div>
</template><script setup lang="ts">
import { ref } from 'vue'
// 场景的数据 
let scene = ref<number>(0)
// 分页器默认页码
let pageNo = ref<number>(1);
//每一页展示几条数据
let pageSize = ref<number>(3);
</script><style scoped></style>

2、业务实现

SPU模块包括:

  • Category组件
  • table表格展示数据
  • spuForm组件(即spu的增删改模块)
  • skuForm组件(即sku的添加删除模块) 

(1) 接口定义

// SPU管理模块的接口
import request from '@/utils/request'
import {SpuData,HasSpuResponseData,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, 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,更新已有的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)

(2)数据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[]
}

SPU模块包括:

  • Category组件
  • table表格展示数据
  • spuForm组件(即spu的增删改模块)
  • skuForm组件(即sku的添加删除模块)

(3)Category组件和table表格展示数据

src/views/product/spu/index.vue 

思路解析:这是场景0,由Category全局组件和一个table表格来展示。

<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 { HasSpuResponseData, 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: HasSpuResponseData = 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>

(4)spuForm组件(即spu的增删改模块)

 src/views/product/spu/spuForm.vue

思路解析:

1.先搭建好静态页面。

2.API书写和接口数据类型定义。

当点击修改时,已有的数据应该呈现在页面。之前我们拿到的数据是不完整的,首先先去写需要请求数据的四个接口以及定义数据ts类型。

3.获取已有的SPU数据。

那么在你点击修改按钮时,去获取并展示数据是最合理的。如何把数据完整的拼在一起呢?如何在父组件拿到子组件的实例对象?父组件用ref,调用子组件实例方法获取完整的SPU数据。然后在子组件方法里面就可以拿到已有的数据,然后子组件也要引入四个接口,这样才能拿到完整的数据。接下来就是存储这些数据。

4.添加与修改(更新)SPU接口与ts数据类型定义。

在你切换页面的时候一定是点击添加或者修改这两个按钮的,所以先去弄下子这俩接口。

5.展示和收集已有的SPU。

当你点击修改按钮时,去展示数据。当父组件把数据传过时,去存储这些数据。把已有的spu赋值给spuparams. 名称用v-model既可以展示又可以收集又可以更新后的数据。 品牌用v-for遍历和:label展示,:valu收集tmid,再用v-model收集到paramsid.描述这里用v-model既收集又展示。spu照片墙数据的搜集展示:v-model直接帮我们收集和展示了:fileList->展示默认图片,action:上传图片的接口地址 , list-type:文件列表的类型。还有具体的自己去看elementplus官网。销售属性展示与收集::data,属性名prop,属性值用插槽el-tag。收集新增属性用,先计算出当前SPU还未拥有的销售属性,用v-model。对于下拉菜单和和添加按钮这具体看代码。以及编辑添加属性值这里用到了编辑与预览模式,用flag和blur切换。细节问操作这里也用插槽。

6.完成更新已有的SPU业务。定义保存方法:先整理数据,因为现在如果直接保存的话,照片墙的数据是直接的name,而不是我们所定义的数据字段imgname。以及接下来我们需要整理销售属性的数据。有一个需要注意的点就是,我们在保存时需要切换场景到到场景0.而且场景0也需要再发送一次请求,获取更新完成后所有的SPU数据。以及给保存按钮加一个禁用的效果:disable。

7.完成添加sp业务。之前完成的是编辑修改功能的SPU现在我们来完成添加SPU效果。即我们添加一个新的SPU之后,在场景0显示出来。首先需要获取并存储数据,但是点击保存完之后再次添加还是会有之前的数据记录,所以就需要清空数据。还有一个注意点就是,修改成功之后,点击保存或者取消,应该返回当前页,而不是第一页。如果是添加就留在第一页。所以在通知父亲切换场景时,就应该告诉父亲是添加还是修改。//更新留在当页 getHasSpu(pageNo.value)添加留在第一页getHasSpu()。

 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>

(5)skuForm组件(即sku的添加删除模块)

1.skuform静态搭建

src/views/product/spu/skuForm.vue

<template><el-form label-width="100px"><el-form-item label="sku名称"><el-input placeholder="请输入名称"></el-input></el-form-item><el-form-item label="sku价格"><el-input placeholder="sku价格" type="number"></el-input></el-form-item><el-form-item label="sku重量"><el-input placeholder="sku重量(克)" type="number"></el-input></el-form-item><el-form-item label="sku描述"><el-input placeholder="请输入描述" type="textarea"></el-input></el-form-item><el-form-item label="平台属性"><el-form :inline="true"><el-form-item label="手机一级"><el-select style="width:200px;"><el-option :label="123"></el-option></el-select></el-form-item><el-form-item label="手机一级"><el-select style="width:200px;"><el-option :label="123"></el-option></el-select></el-form-item><el-form-item label="手机一级"><el-select style="width:200px;"><el-option :label="123"></el-option></el-select></el-form-item></el-form></el-form-item><el-form-item label="销售属性"><el-form :inline="true"><el-form-item label="手机一级"><el-select style="width:200px;"><el-option :label="123"></el-option></el-select></el-form-item><el-form-item label="手机一级"><el-select style="width:200px;"><el-option :label="123"></el-option></el-select></el-form-item><el-form-item label="手机一级"><el-select style="width:200px;"><el-option :label="123"></el-option></el-select></el-form-item></el-form></el-form-item><el-form-item label="图片"><el-table border><el-table-column type="selection" width="80px" align="center"></el-table-column><el-table-column label="图片"></el-table-column><el-table-column label="名称"></el-table-column><el-table-column label="操作"></el-table-column></el-table></el-form-item><el-form-item><el-button type="primary" size="default">保存</el-button><el-button type="primary" size="default" @click="cancel">取消</el-button></el-form-item></el-form>
</template><script setup lang="ts">
let $emit=defineEmits(['changeScene'])
// 取消按钮的回调
const cancel=()=>{$emit('changeScene',{flag:0,params:''})
}
</script><style scoped></style>
2.获取添加sku数据展示

当你点击添加按钮,需要先获取到三个数据,分别是销售属性,平台属性,图片。所以要发请求获取这三个数据。之这三个接口我们都有。

1.在子组件skuform中添加initSkuData方法,对外暴露。在父组件获取子组件实例SkuForm,调用子组件的方法,初始化添加SKU的数据。子组件中收到c1Id,c2Id,spu。
2.接下来发请求,获取这三个数据。引入这三个请求的API;获取平台属性,销售属性,照片墙数据;在skuform中定义这三个字段,存储数据;展示数据,平台、销售属性的展示在el-form-item和el-option用v-for遍历,图片的展示用:data="imgArr"先弄出来有几个,图片的展示用插槽,名称用prop展示。
3.收集新增sku的参数。在接口index.ts中追加一个新增的sku地址,添加sku的方法,定义好要新增的sku数据类型;在skuform中用skuParams收集sku的参数id;名字、价格、重量、描述用v-model收集;
4.平台属性的收集:在option收集属性id和属性值id,:value="`${item.id}:${attrValue.id}`";先把这些id收集到平台属性对象身上,el-select中 v-model="item.attrIdAndValueId";
同理销售属性的收集:el-option :value="`${item.id}:${saleAttrValue.id}`";与平台属性一致同理el-select v-model="item.saleIdAndValueId";
设置默认图片的收集:绑定点击事件@click="handler(row)";收集图片地址 skuParams.skuDefaultImg = row.imgUrl;toggleRowSelection用这个api选中复选框;
5.数据收集完毕,点击保存按钮发送请求。定义save保存方法;整理参数,因为之前收集到的平台和销售属性都定义到了一个属性里面,所以用reduce把他切成两个id,用我们自己定义的id再去存储一下然后push到数组里面;接下来添加SKU的请求let result: any = await reqAddSku(skuParams)并提示成功失败和切换场景。
6.sku列表的展示:场景0里面右侧有一个眼睛按钮可以查看sku列表;通过dialog和table去展示。
首先去index.ts里面添加一个查看sku的接口和方法;然后去定义获取sku数据接口的ts类型;在场景0里面定义查看的方法@click="findSku(row)",把数据存储在SkuInfoData中,如果数据获取成功就将dialog显示出来;
7.spu模块删除模块:根据已有的spu数据id删除,成功之后再发送一次请求获取最新的spu数据;在index.ts里面定义一个删除已有的spu的接口和方法;在场景0里面用气泡el-popconfirm;定义一个确定方法@confirm="deleteSpu(row)",而且要在获取一次最新的数据并返回当前页或者上一页getHasSpu(records.value.length > 1 ? pageNo.value : pageNo.value - 1);且路由组件销毁前,清空仓库关于分类的数据。

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管理模块

1.静态搭建。
用到el-table和el-table-column以及分页器。右侧是固定的所以用fixed="right"固定操作这一序列在右侧。
2.获取、存储、展示已有SKU数据。
(1)首先定义接口和数据。具体看下面代码。
(2)引入请求接口,获取数据。
(3)存储数据let result: SkuResponseData = await reqHasSku(pageNo.value, pageSize.value)
(4)一上来就应该展示sku数据,在el-table中:data="skuAttr",在el-pagination中用:total="total"展示已有数据;其他的序号名称描述重量价格用prop来展示,图片用插槽展示,四个按钮用作用域插槽展示。
(5)下拉菜单展示变化,@size-change="handler",
3.SKU的上架与下架
(1):icon="row.isSale === 1 ? 'Bottom' : 'Top'"
用这个展示是上架还是下架
(2)在sku/index.ts写好上架下架的接口与方法
(3) @click="updateSale(row)"绑定点击事件,判断isSale是0还是1,以及判断信息提示,和再次请求获取新的数据,否则更新不及时。
4.编辑按钮
(1)@click="updateSku"
(2)其实这个没有业务,只要弹出消息ElMessage在更新中就OK了
5.查看商品详情
(1)静态搭建
是一个抽屉效果。使用Element-plus的Drawer组件。
轮播图也使用插件,Carousel走马灯
(2)在查看图标这里绑定查看方法findSku
(3)展示商品详情
点击时应该拿到商品的信息,拿到数据去展示。
首先去sku/index.ts定义获取商品详情的接口和方法以及定义数据ts类型。
获取已有商品详情数据let result: SkuInfoData = await reqSkuInfo(row.id as number)。
如果获取成功的话去存储已有的SKUskuInfo.value = result.data。
数据也要换成动态的,名称描述等,这样来展示{{ skuInfo.skuName }};平台属性用v-for遍历,在展示属性值;销售属性同理
6.删除已有的商品
(1)写好相应的接口方法
(2)用到了气泡渲染框
(3)绑定事件@confirm="removeSku(row.id),进行判断和提示,成功之后再次发送请求更新数据,并判断停留在当前页还是上一页。
 

① 接口定义 

 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
}

 3.业务实现

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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/5324.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

根据标签最大层面ROI提取原始图像区域

今天要实现的任务是提取肿瘤的感兴趣区域。 有两个文件&#xff0c;一个是nii的原始图像文件&#xff0c;一个是nii的标签文件。 我们要实现的是&#xff1a;在标签文件上选出最大层面&#xff0c;然后把最大层面的ROI映射到原始图像区域&#xff0c;在原始图像上提裁剪出ROI…

PLC通过Modbus转Profinet网关连接变频器与电机通讯

Modbus转Profinet网关&#xff08;XD-MDPN100&#xff09;是一种能够实现Modbus协议和Profinet协议之间转换的设备。Modbus转Profinet网关可提供单个或多个RS485接口&#xff0c;PLC作为控制中枢&#xff0c;变频器作为控制电机转速&#xff0c;通过Modbus转Profinet网关&#…

瑞米派实时系统与EtherCAT移植-米尔Remi Pi

1.概述 Remi Pi采用瑞萨RZ/G2L作为核心处理器&#xff0c;该处理器搭载双核Cortex-A551.2GHzCortex-M33200MHz处理器&#xff0c;其内部集成高性能3D加速引擎Mail-G31 GPU(500MHz)和视频处理单元&#xff08;支持H.264硬件编解码&#xff09;,16位的DDR4-1600 / DDR3L-1333内存…

Webshell绕过技巧分析之-base64编码和压缩编码

在网络安全运营&#xff0c;护网HVV&#xff0c;重保等活动的过程中&#xff0c;webshell是一个无法绕过的话题。通常出现的webshell都不是以明文的形式出现&#xff0c;而是针对webshell关键的内容进行&#xff0c;混淆&#xff0c;编码来绕过网络安全产品&#xff0c;例如IDS…

计算机提示msvcp110.dll是什么意思?msvcp110.dll丢失恢复办法

在Windows操作系统中&#xff0c;动态链接库&#xff08;DLL&#xff09;扮演着至关重要的角色&#xff0c;它们是实现程序间代码共享和模块化设计的关键组件。msvcp110.dll&#xff0c;作为Microsoft Visual C 2012运行时库的一个组成部分&#xff0c;是理解现代软件开发和维护…

【酱浦菌-爬虫技术细节】解决学术堂爬虫翻页(下一页)问题

首先我们通过css选择器获取页码信息&#xff0c;这里的css选择器&#xff0c;选择的是含有a标签的所有li标签&#xff0c;代码如下&#xff1a; li html_web.css(div.pd_c_xslb_left_fenye ul li>a) for li in li:li_url li.css(a::attr(href)).get()li_num li.css(a::t…

STM32入门_江协科技_3~4_OB记录的自学笔记_软件安装新建工程

3. 软件安装 3.1. 安装Keil5 MDK 作者的资料下载的连接如下&#xff1a;https://jiangxiekeji.com/download.html#32 3.2. 安装器件支持包 因为新的芯片层出不穷&#xff0c;所以需要安装Keil5提供的器件升级版对软件进行升级&#xff0c;从而支持新的芯片&#xff1b;如果不…

unity-C#调用百度千帆AppBuilder的OpenApi

目录 功能描述准备工作百度智能云账号创建应用编辑应用创建Api秘钥Api调用流程unity代码Unitywebrequest非流式流式注意事项 Restsharp 功能描述 使用百度千帆AppBuilder平台,通过api调用的方式实现AI大模型对话功能(文字) 准备工作 百度智能云账号 请自行在百度智能云进行…

力扣---二叉树的右视图

给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 示例 1: 输入: [1,2,3,null,5,null,4] 输出: [1,3,4]示例 2: 输入: [1,null,3] 输出: [1,3]示例 3: 输入: [] 输出: []实现方法&…

nginx+Tomcat动静分离

本⽂的动静分离主要是通过nginxtomcat来实现&#xff0c;其中nginx处理图⽚、html等静态的⽂ 件&#xff0c;tomcat处理jsp、do等动态⽂件. 实验环境 192.168.200.133 nginx反向代理 192.168.200.129 static 192.168.200.130 dynamic 步骤 修改三台主机名 [rootadmin ~]#…

关于Centos 7/8 网络设置 与工具连接

网络三步曲的配置 1、首先更改虚拟机的网络配置 查看子网地址以及网关 如果有要求需要更改IP地址&#xff0c;规定第三位是指定数值&#xff0c;那么需要全部更改 例如&#xff0c;IP地址为192.168.200.30 其中200为重点&#xff0c;更改时为以下步骤 1、点击DHCP设置&#x…

【数据结构】顺序表专题

前言 本篇文章我们来进行有关顺序表的专题训练&#xff0c;让我们一起来看一下有关顺序表的算法题 &#x1f493; 个人主页&#xff1a;小张同学zkf ⏩ 文章专栏&#xff1a;数据结构 &#x1f4dd;若有问题 评论区见 &#x1f389;欢迎大家点赞&#x1f44d;收藏⭐文章 1.移除…

每天五分钟深度学习:导数是反向传播算法的数学基础

本文重点 导数作为微积分学的核心概念之一,不仅在数学领域内占有举足轻重的地位,更在实际问题中发挥着不可替代的作用。我们要想学习反现象传播算法,我们前提是先要学习导数的概念。本节课程我们将看一下导数是什么? 导数 导数,顾名思义,是函数在某一点或某一段区间内…

【国标语音对讲】EasyCVR视频汇聚平台海康/大华/宇视摄像头GB28181语音对讲配置

一、背景分析 近年来&#xff0c;国内视频监控应用发展迅猛&#xff0c;系统接入规模不断扩大&#xff0c;涌现了大量平台提供商&#xff0c;平台提供商的接入协议各不相同&#xff0c;终端制造商需要给每款终端维护提供各种不同平台的软件版本&#xff0c;造成了极大的资源浪…

值得买科技新思路,导购电商的终点是“AI+出海”?

在以往&#xff0c;大众普遍认为品牌的消费者大多是高度忠诚人群&#xff0c;而事实上&#xff0c;非品牌忠诚者相比重度消费者&#xff0c;对促进品牌增长更为重要。 这类非品牌忠诚者被定义为摇摆的消费者群体&#xff0c;也就是那些购买品牌产品概率在20%-80%之间的消费者。…

mysql-sql练习-5-行列互转

目录 成绩单 简单互转 需求 多行转多列 分组 判断 聚合 理解 分组 合并 逆向需求 多列转多行 输出 合并 abc 去重 合并 拆分 需求 建表 多行转多列 逆向需求 多列转多行 拆分 按长度 拆分 按个数 成绩单 简单互转 需求 多行转多列 分组 判断 聚合 with tmp as(--…

[Transactional Level Bypass] Bypass Validation Rule in Apex Batch Class

问题 现有一个batch job用于批量更新Lead&#xff0c;最近频繁收到apex exception email, 显示更新Lead的时候触发了validation rule&#xff0c;导致apex job运行失败。 batch class节选如下&#xff1a; public void execute(Database.BatchableContext bc, List<Lead&…

49. 字母异位词分组 128. 最长连续序列

49. 字母异位词分组 128. 最长连续序列 把集合里面的所有元素都放入set容器里面 定义结果最大连续数量 ans for循环遍历每个元素 先判断集合里面有没有比这个元素小1的 如果没有 说明这个元素就是序列的第一个元素 然后接着找集合里面有没有比这个元素大1的 while一直找 …

C语言中的趣味代码(五)

我想以此篇结束关于C语言的博客&#xff0c;因为在C语言拖得越久越不能给大家带来新的创作&#xff0c;在此我也相信大家对C语言已经有了一个新的认知。进入正题&#xff0c;在这一篇中我主要编一个“英语单词练习小程序”来给大家展开介绍&#xff0c;从测试版逐步改良&#x…

【无标题】测试下目录间距

文章目录 前言一、Java基础题1、Java语言的三大特性2、JDK 和 JRE 有什么区别3、Java基本数据类型及其封装类4、说明一下public static void main(String args[])这段声明里关键字的作用5、 与 equals 的区别6、Java语言的三大特性7、Java语言的三大特性8、Java语言的三大特性9…