day05
替换真实接口
真实接口地址:http://sph-api.atguigu.cn
添加到三台服务器中:开发、测试、上线(添加变量,我们不用手动去变化)
跨域问题
vite官网的配置
loadEnv:加载接口变量,调用该函数会返回当前的环境变量
mode告诉加载哪个环境的文件,process告诉文件在哪里,这样就会获取对应的一个环境对象
project\vite.config.ts
import { defineConfig, loadEnv } from 'vite'
export default defineConfig(({ command, mode }) => {
// 获取各个环境下的变量,mode告诉加载哪个环境的文件,process告诉文件在哪里,这样就会获取对应的一个环境对象
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/, ''),
}
}
}
}
})
真实接口替换api中mock的接口,类型也要重新定义一下
project\src\api\user\index.ts
// 统一管理用户相关的接口
// 发请求就需要 request
import request from '@/utils/request'
// 用户相关的接口
enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout'
}
// 暴露请求函数
// 登入的接口方法
export const reqLogin = (data: any) => request.post<any, any>(API.LOGIN_URL, data)
// 获取用户信息的接口方法
export const reqUserInfo = () => request.get<any, any>(API.USERINFO_URL)
//退出登入的接口
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
去用户小仓库中修改三个发请求的函数
体验账号:admin,atguigu123
login路由组件中的初始密码记得修改一下,方便登入
project\src\store\modules\user.ts
// 用户相关的小仓库
import { defineStore } from 'pinia'
// 引入登入请求接口
import { reqLogin, reqUserInfo, reqLogout } from '@/api/user/index'
// 创建用户小仓库
let useUserStore = defineStore('User', {
// 小仓库存储数据的地方
state: (): UserState => {
return {
token: GET_TOKEN(),//用户的唯一标识token
menuRoutes: constantRoute,// 仓库存储生成菜单需要的数组(路由配置数组)
username: '',
avatar: ''
}
},
// 异步、逻辑
actions: {
// 用户登入的方法
async userLogin(data: any) {
let res: any = await reqLogin(data)
//登入请求:成功200----token
//登入请求:失败201--登入失败错误的信息
if (res.code === 200) {
// pinia仓库存储token
// pinia|vuex存储数据都是利用js对象,并非持久化 cookie也可以持久化
this.token = (res.data as string)
// 本地持久化存储一份
SET_TOKEN((res.data as string))
// 能保证当前async函数返回一个成功的promise
return 'ok'
} else {
return Promise.reject(new Error(res.data))
}
},
// 拿token向服务器请求用户数据
async getUserInfo() {
// 获取用户信息进行存储【头像,用户名】
let res = await reqUserInfo()
if (res.code == 200) {
this.username = res.data.name
this.avatar = res.data.avatar
return 'ok'
} else {
return Promise.reject(new Error(res.message))
}
},
//退出登入
async userLogout() {
let res = await reqLogout()
if (res.code == 200) {
//清除数据
this.token = ''
this.username = ''
this.avatar = ''
localStorage.removeItem('TOKEN')
return 'ok'
} else {
return Promise.reject(new Error(res.message))
}
}
},
getters: {
}
})
export default useUserStore
到tabbar右侧组件中,根据退出登入返回的promise结果进行操作,推出登入成功则跳转到login
路由鉴权中,token过期也需要等待结果,保证退出登入成功之后再跳转login(加上await)
project\src\layout\tabbar\setting\index.vue
const logout = async () => {
await userStore.userLogout();
//跳转到登录页面
$router.push({ path: '/login' });
}
定义接口的ts数据类型
接口中数据类型定义好之后,仓库中的数据类型也要改
//定义用户相关数据的ts类型
//用户登录接口携带参数的ts类型
export interface loginFormData {
username: string
password: string
}
//定义全部接口返回数据都拥有ts类型
export interface ResponseData {
code: number
message: string
ok: boolean
}
//定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {
data: string
}
//定义获取用户信息返回数据类型
export interface userInfoReponseData extends ResponseData {
data: {
routes: string[]
buttons: string[]
roles: string[]
name: string
avatar: string
}
}
品牌模块搭建
使用element搭建
project\src\views\product\trademark\index.vue
<template>
<el-card class="box-card">
<!-- 卡片顶部添加品牌按钮 -->
<el-button color="#c6d182" size="default" icon="Plus">添加品牌</el-button>
<!-- 表格组件:用于展示已有得平台数据 -->
<!-- table:---border:可以设置表格纵向是否有边框
table-column:---label:某一个列表 ---width:设置这列宽度 ---align:设置这一列对齐方式
-->
<el-table style="margin:10px 0px" border>
<el-table-column label="序号" width="80px" align="center" type="index"></el-table-column>
<!-- table-column:默认展示数据用div -->
<el-table-column label="品牌名称"></el-table-column>
<el-table-column label="品牌LOGO"></el-table-column>
<el-table-column label="品牌操作"></el-table-column>
</el-table>
<!-- 分页器组件
pagination
v-model:current-page:设置分页器当前页码
v-model:page-size:设置每一个展示数据条数
page-sizes:用于设置下拉菜单数据
background:设置分页器按钮的背景颜色
layout:可以设置分页器六个子组件布局调整
-->
<el-pagination :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="400" />
</el-card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
//当前页码
let pageNo = ref<number>(1);
//每一页展示多少条数据
let limit = ref<number>(3);
</script>
<style scoped lang="scss"></style>
发请求,渲染数据
每次点击分页器都发一次请求,哪一页,要几条
在组件中发请求封装成一个函数,需要请求就调用。
组件挂载完毕发一次,点击分页器发一次
project\src\views\product\trademark\index.vue
import { ref, onMounted } from 'vue'
//请求接口函数
import { reqHasTrademark } from '@/api/product/trademark'
//当前页码
let pageNo = ref<number>(1);
//每一页展示多少条数据
let limit = ref<number>(3);
//存储已有品牌数据总数
let total = ref<number>(0);
// 存储已有品牌的数据
let trademarkArr = ref<any>([])
// 获取已有品牌的接口---封装成一个函数,需要数据就调用
const getHasTrademark = async () => {
let res = await reqHasTrademark(pageNo.value, limit.value)
console.log(res)
if (res.code == 200) {
total.value = res.data.total
trademarkArr.value = res.data.records
}
}
//组件挂载发请求
onMounted(() => {
getHasTrademark()
})
什么时候用prop,什么时候用插槽:prop就是简单的用div展示数据,想要跟多选择就用插槽
<el-table style="margin:10px 0px" 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, $index }">
<img :src="row.logoUrl" style="width:100px;height: 100px;">
</template>
</el-table-column>
<el-table-column label="品牌操作">
<template #="{ row, $index }">
<el-button size="small" icon="Edit"></el-button>
</template>
</el-table-column>
</el-table>
定义品牌ts类型
project\src\api\product\trademark\type.ts
export interface ResponseData {
code: number
message: string
ok: boolean
}
//已有的品牌的ts数据类型
export interface TradeMark {
id?: number
tmName: string
logoUrl: string
}
//包含全部品牌数据的ts类型
export type Records = TradeMark[]
//获取的已有全部品牌的数据ts类型
export interface TradeMarkResponseData extends ResponseData {
data: {
records: Records
total: number
size: number
current: number
searchCount: boolean
pages: number
}
}
对应的index文件的类型需要修改,还有组件中发请求的时候类型一同修改。
小问题:已有品牌数据为什么不放在仓库中发请求呢。我觉得是因为这些数据不需要共享,组件自己用,直接在组件中发请求获取就行
分页器展示数据
@size-change="sizeChange" @current-change="changePageNo"
//分页器页码发生变化时触发
const changePageNo = () => {
//当前页码发生变化的时候再次发请求获取对应已有品牌数据展示
getHasTrademark();
}
//当下拉菜单发生变化的时候触发次方法
//这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
//当前每一页的数据量发生变化的时候,当前页码归1
pageNo.value = 1
getHasTrademark();
}
添加按钮对话框实现
增加品牌和修改和删除品牌的接口
project\src\api\product\trademark\index.ts
//书写品牌管理模块接口
import request from '@/utils/request'
import type { TradeMarkResponseData, TradeMark } from './type'
//品牌管理模块接口地址
enum API {
//获取已有品牌接口
TRADEMARK_URL = '/admin/product/baseTrademark/',
//添加品牌
ADDTRADEMARK_URL = '/admin/product/baseTrademark/save',
//修改已有品牌
UPDATETRADEMARK_URL = '/admin/product/baseTrademark/update',
//删除已有品牌
DELETE_URL = '/admin/product/baseTrademark/remove/',
}
//获取已有品牌的接口方法
//page:获取第几页 ---默认第一页
//limit:获取几个已有品牌的数据
export const reqHasTrademark = (page: number, limit: number) =>
request.get<any, TradeMarkResponseData>(
API.TRADEMARK_URL + `${page}/${limit}`,
)
//添加与修改已有品牌接口方法
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)
}
}
//删除某一个已有品牌的数据
export const reqDeleteTrademark = (id: number) =>
request.delete<any, any>(API.DELETE_URL + id)
重点:
1 新增品牌
通过参数收集新增品牌的名字和图片地址,然后点击确定按钮将数据通过请求发给服务器,新增一个品牌
而且新增之后,需要重新获取一下所有品牌的数据,更新一下分页器组件
在点击添加品牌的时候清空数据,只要一次(后面清除验证错误信息也一样,不过需要nextTick)
2 修改品牌
通过rew拿到现有的品牌信息展示在对话框中,就是赋值给trademarkParams数据
修改品牌需要id
通过判断有没有id区分新增还是修改,对应对话框标题
3 表单校验
ref="formRef",做校验需要获取到组件实例对象
点击确定按钮的时候需要对整个表单校验
需要清除验证规则错误的提示信息
nextTick 数据更新,获取更新后的dom
4 删除品牌
project\src\views\product\trademark\index.vue
<template>
<div>
<el-card class="box-card">
<!-- 卡片顶部添加品牌按钮 -->
<el-button color="#c6d182" size="default" icon="Plus" @click="addTrademark">添加品牌</el-button>
<!-- 表格组件:用于展示已有得平台数据 -->
<!-- table:---border:可以设置表格纵向是否有边框
table-column:---label:某一个列表 ---width:设置这列宽度 ---align:设置这一列对齐方式
-->
<el-table style="margin:10px 0px" 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="品牌操作">
<template #="{ row }">
<el-button size="small" icon="Edit" @click="updateTrademark(row)"></el-button>
<el-popconfirm :title="`您确定要删除${row.tmName}?`" width="250px" icon="Delete"
@confirm='removeTradeMark(row.id)'>
<template #reference>
<el-button type="primary" size="small" icon="Delete"></el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 分页器组件
pagination
v-model:current-page:设置分页器当前页码
v-model:page-size:设置每一个展示数据条数
page-sizes:用于设置下拉菜单数据
background:设置分页器按钮的背景颜色
layout:可以设置分页器六个子组件布局调整
-->
<el-pagination @size-change="sizeChange" @current-change="changePageNo" :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>
<!-- 对话框组件:在添加品牌与修改已有品牌的业务时候使用结构 -->
<!--
v-model:属性用户控制对话框的显示与隐藏的 true显示 false隐藏
title:设置对话框左上角标题
-->
<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">
<!-- upload组件属性:action图片上传路径书写/api,代理服务器不发送这次post请求 -->
<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>
</div>
</template>
<script setup lang="ts">
import { ElMessage, UploadProps, formEmits } from 'element-plus'
import { ref, onMounted, reactive, nextTick } from 'vue'
//请求接口函数
import { reqHasTrademark, reqAddOrUpdateTrademark, reqDeleteTrademark } from '@/api/product/trademark'
import type { Records, TradeMarkResponseData, 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: ''
})
//获取el-form组件实例
let formRef = ref();
// 获取已有品牌的接口---封装成一个函数,需要数据就调用
const getHasTrademark = async (page = 1) => {
let res: TradeMarkResponseData = await reqHasTrademark(pageNo.value, limit.value)
console.log(res)
if (res.code == 200) {
total.value = res.data.total
trademarkArr.value = res.data.records
}
}
//组件挂载发请求
onMounted(() => {
getHasTrademark()
})
//分页器页码发生变化时触发
const changePageNo = () => {
//当前页码发生变化的时候再次发请求获取对应已有品牌数据展示
getHasTrademark();
}
//当下拉菜单发生变化的时候触发次方法
//这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
//当前每一页的数据量发生变化的时候,当前页码归1
pageNo.value = 1
getHasTrademark();
}
//添加品牌按钮的回调
const addTrademark = () => {
//对话框显示
dialogFormVisible.value = true;
//清空收集数据
trademarkParams.id = 0;
trademarkParams.tmName = '';
trademarkParams.logoUrl = '';
nextTick(() => {
formRef.value.clearValidate('tmName');
formRef.value.clearValidate('logoUrl');
})
}
//修改已有品牌的按钮的回调
//row:row即为当前已有的品牌
const updateTrademark = (row: TradeMark) => {
dialogFormVisible.value = true;
//修改品牌时需要id的
trademarkParams.id = row.id
//展示已有数据
trademarkParams.tmName = row.tmName
trademarkParams.logoUrl = row.logoUrl
//第一种写法:ts的问号语法
// formRef.value?.clearValidate('tmName');
// formRef.value?.clearValidate('logoUrl');
nextTick(() => {
formRef.value.clearValidate('tmName');
formRef.value.clearValidate('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 4M
if (rawFile.type == 'image/png' || rawFile.type == 'image/jpeg' || rawFile.type == 'image/gif') {
// 文件大小限制
if (rawFile.size / 1024 / 1024 < 4) {
return true;
} else {
ElMessage({
type: 'error',
message: '上传文件大小小于4M'
})
return false;
}
} else {
ElMessage({
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 = {
tmName: [
//required:这个字段务必校验,表单项前面出来五角星
//trigger:代表触发校验规则时机[blur、change]
{ required: true, trigger: 'blur', validator: validatorTmName }
],
logoUrl: [
{ required: true, validator: validatorLogoUrl }
]
}
//气泡确认框确定按钮的回调
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: '删除品牌失败'
})
}
}
</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>
属性模块搭建
需要4个接口:获取一级分类,携带一级分类id获取二级分类,携带二级分类id获取三级分类,携带123级分类获取表格数据展示
将三级导航分类注册为全局组件,其他的也可以用
属性相关接口的文件,和类型定义
project\src\api\product\attr\index.ts
//这里书写属性相关的API文件
import request from '@/utils/request'
import type { CategoryResponseData, AttrResponseData, Attr } from './type'
//属性管理模块接口地址
enum API {
//获取一级分类接口地址
C1_URL = '/admin/product/getCategory1',
//获取二级分类接口地址
C2_URL = '/admin/product/getCategory2/',
//获取三级分类接口地址
C3_URL = '/admin/product/getCategory3/',
//获取分类下已有的属性与属性值
ATTR_URL = '/admin/product/attrInfoList/',
//添加或者修改已有的属性的接口
ADDORUPDATEATTR_URL = '/admin/product/saveAttrInfo',
//删除某一个已有的属性
DELETEATTR_URL = '/admin/product/deleteAttr/',
}
//获取一级分类的接口方法
export const reqC1 = () => request.get<any, CategoryResponseData>(API.C1_URL)
//获取二级分类的接口方法
export const reqC2 = (category1Id: number | string) =>
request.get<any, CategoryResponseData>(API.C2_URL + category1Id)
//获取二级分类的接口方法
export const reqC3 = (category2Id: number | string) =>
request.get<any, CategoryResponseData>(API.C3_URL + category2Id)
//获取对应分类下已有的属性与属性值接口
export const reqAttr = (
category1Id: string | number,
category2Id: string | number,
category3Id: string | number,
) =>
request.get<any, AttrResponseData>(
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)
接口类型定义
project\src\api\product\attr\type.ts
//分类相关的数据ts类型
export interface ResponseData {
code: number
message: string
ok: boolean
}
//分类ts类型
export interface CategoryObj {
id: number | string
name: string
category1Id?: number
category2Id?: number
}
//相应的分类接口返回数据的类型
export interface CategoryResponseData extends ResponseData {
data: CategoryObj[]
}
全局组件:Category
组件挂载获得一级数据,何时获取二级呢
方法一:检测仓库中收集的一级id有没有发生变化
方法二:element下来菜单自带的事件 change
project\src\components\Category\index.vue
<template>
<div>
<el-card>
<el-form :inline="true">
<el-form-item label="一级分类">
<!-- change:选中值发生变化时触发 -->
<el-select :disabled="scene == 0 ? false : true" 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 == 0 ? false : true" 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 == 0 ? false : true" 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>
</div>
</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 lang="scss"></style>
属性组件
根据分类全局组件的三个id去获取属性数据(id在仓库中
监听三级分类的id,如果有就去发请求获取属性数据
一级分类发生改变,会导致二级id没有,也就没有三级id,此时不应该发请求,而且需要清空下面的展示内容
设一个控制变量scene,进行添加和展示卡片的切换(编辑也一样
需要把场景scene传递给子组件Category,在场景一中(添加或编辑)禁用下拉
添加属性和修改属性用的一个接口,区别在于有没有id
用变量收集新增的属性,发请求的时候带过去
给每个属性值对象添加一个flag属性,这样就可以控制编辑和查看功能(只使用一个变量的话会互相影响)
表单聚焦:通过ref拿到组件实例(注意表单flag刚切换可能拿不到实例,需要nextTick),使用element表单方法focus
属性值修改:JSON.parse(JSON.stringify(row))实现深拷贝,就是拷贝独立的一份,不让它影响原来的数据(业务场景:浅拷贝了原数据,修改当前数据,点击取消,但原数据改变导致小bug,所以我们用深拷贝,独立拷贝一份给他,这样不影响原数据)
删除属性操作:也要接口,根据属性id删除
project\src\views\product\attr\index.vue
<template>
<div>
<Category :scene="scene" />
<el-card style="margin: 10px 0px;">
<div v-show="scene == 0">
<el-button @click="addAttr" size="default" icon="Plus"
:disabled="categoryStore.c3Id ? false : true">添加属性</el-button>
<el-table border style="margin: 10px 0px;" :data="attrArr">
<el-table-column label="序号" align="center" type="index" width="80px"> </el-table-column>
<el-table-column label="属性名称" width="120px" prop="attrName"></el-table-column>
<el-table-column label="属性值名称">
<template #="{ row, $index }">
<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, $index }">
<!-- 修改已有属性的按钮 -->
<el-button type="primary" 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="primary" 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 @click="addAttrValue" :disabled="attrParams.attrName ? false : true" type="primary"
size="default" icon="Plus">添加属性值</el-button>
<el-button type="primary" size="default" @click="cancel">取消</el-button>
<el-table border style="margin:10px 0px" :data="attrParams.attrValueList">
<el-table-column label="序号" width="80px" type="index" align="center"></el-table-column>
<el-table-column label="属性值名称">
<!-- row:即为当前属性值对象 -->
<template #="{ row, $index }">
<el-input :ref="(vc: any) => inputArr[$index] = vc" v-if="row.flag" @blur="toLook(row, $index)"
size="small" 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 #="{ row, index }">
<el-button type="primary" 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 type="primary" size="default" @click="cancel">取消</el-button>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { watch, ref, reactive, nextTick, onBeforeUnmount } from 'vue';
// 引入获取已有属性与属性值接口
import { reqAttr, reqAddOrUpdateAttr, reqRemoveAttr } from '@/api/product/attr';
//获取分类的仓库
import useCategoryStore from '@/store/modules/category';
import { AttrResponseData, Attr, AttrValue } from '@/api/product/attr/type';
import { ElMessage } from 'element-plus';
let categoryStore = useCategoryStore();
//存储已有的属性与属性值
let attrArr = ref<Attr[]>([]);
//定义card组件内容切换变量
//scene=0,显示table,scene=1,展示添加与修改属性结构
let scene = ref<number>(0);
//收集新增的属性的数据
let attrParams = reactive<Attr>({
attrName: "",//新增的属性的名字
attrValueList: [//新增的属性值数组
],
categoryId: '',//三级分类的ID
categoryLevel: 3,//代表的是三级分类
})
//准备一个数组:将来存储对应的组件实例el-input
let inputArr = ref<any>([]);
//监听仓库三级id的变化
watch(() => categoryStore.c3Id, () => {
//清空上一次查询的属性与属性值
attrArr.value = [];
//保证三级分类得有才能发请求
if (!categoryStore.c3Id) return;
getAttr()
})
//获取已有的属性与属性值方法
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: "",//新增的属性的名字
attrValueList: [//新增的属性值数组
],
categoryId: categoryStore.c3Id,//三级分类的ID
categoryLevel: 3,//代表的是三级分类
})
//切换为添加与修改属性的结构
scene.value = 1;
}
//table表格修改已有属性按钮的回调
const updateAttr = (row: Attr) => {
//切换为添加与修改属性的结构
scene.value = 1;
//将已有的属性对象赋值给attrParams对象即为
//ES6->Object.assign进行对象的合并,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) => {
//非法情况判断1
if (row.valueName.trim() == '') {
//删除调用对应属性值为空的元素
attrParams.attrValueList.splice($index, 1);
//提示信息
ElMessage({
type: 'error',
message: '属性值不能为空'
})
return;
}
//非法情况2
let 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,展示div
row.flag = false;
}
//属性值div点击事件
const toEdit = (row: AttrValue, $index: number) => {
//相应的属性值对象flag:变为true,展示input
row.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 lang="scss"></style>
id涉及子组件给父组件传数据----category组件的id父组件attr也要用
所以三个级别的id放在仓库中比较合适,谁用都方便
project\src\store\modules\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'
const useCategoryStore = defineStore('Category', {
state: (): CategoryState => {
return {
//存储一级分类的数据
c1Arr: [],
//存储一级分类的ID
c1Id: '',
//存储对应一级分类下二级分类的数据
c2Arr: [],
//收集二级分类的ID
c2Id: '',
//存储三级分类的数据
c3Arr: [],
//存储三级分类的ID
c3Id: '',
}
},
actions: {
//获取一级分类的方法
async getC1() {
//发请求获取一级分类的数据
const result: CategoryResponseData = await reqC1()
if (result.code == 200) {
this.c1Arr = result.data
}
},
//获取二级分类的数据
async getC2() {
//获取对应一级分类的下二级分类的数据
const result: CategoryResponseData = await reqC2(this.c1Id)
if (result.code == 200) {
this.c2Arr = result.data
}
},
//获取三级分类的数据
async getC3() {
const result: CategoryResponseData = await reqC3(this.c2Id)
if (result.code == 200) {
this.c3Arr = result.data
}
},
},
getters: {},
})
export default useCategoryStore
仓库类型定义
project\src\store\modules\types\type.ts
import type { CategoryObj } from '@/api/product/attr/type'
//定义分类仓库state对象的ts类型
export interface CategoryState {
c1Id: string | number
c1Arr: CategoryObj[]
c2Arr: CategoryObj[]
c2Id: string | number
c3Arr: CategoryObj[]
c3Id: string | number
}