【Vue3+TS项目】硅谷甄选day05--真实接口+跨域+品牌模块搭建+属性模块搭建(深拷贝、nextTick)

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
}

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值