Skip to content

后台管理项目

平台属性页面

分类全局组件

创建分类组件:src/components/Category/index.vue

vue
<template>
    <el-card>
        <!-- 设置行内 -->
        <el-form :inline="true">
            <el-form-item label="一级分类">
                <el-select >
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option label="Zone one" value="shanghai" />
                    <el-option label="Zone two" value="beijing" />
                </el-select>
            </el-form-item>
            
            <el-form-item label="二级分类">
                <el-select >
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option label="Zone one" value="shanghai" />
                    <el-option label="Zone two" value="beijing" />
                </el-select>
            </el-form-item>

            <el-form-item label="三级分类">
                <el-select >
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option label="Zone one" value="shanghai" />
                    <el-option label="Zone two" value="beijing" />
                </el-select>
            </el-form-item>

        </el-form>
    </el-card>
</template>

<script setup lang='ts'>
 
</script>

<style scoped lang="less">
</style>

在入口文件引入,挂载为全局组件:src/main.ts

ts
import Category from 'src/components/Category/index.vue'
// 挂载为全局组件
app.component('Category',Category)

在平台属性组件中使用分类组件:src/views/product/attr/index.vue

vue
<template>
    <Category></Category>
    <el-card style="margin: 10px 0px">xxx</el-card>
</template>

<script setup lang='ts'>

</script>

<style scoped lang="less">
</style>

获取分类信息

封装分类数据api: src/api/product/category.ts

tsx
import request from '@/utils/request';
import { CategoryResponseData } from './type/category';

//枚举地址
enum API {
    //获取一级分类的GET请求:不需要携带任何参数
    GETC1_URL="/admin/product/getCategory1",
    //获取二级分类的数据GET请求:需要携带一级分类的ID
    GETC2_URL="/admin/product/getCategory2/",
    //获取三级分类的数据GET请求:需要携带二级分类的ID
    GETC3_URL="/admin/product/getCategory3/"
}

// 获取一级分类
export const getC1 = () => {
    return request.get<any,CategoryResponseData>(API.GETC1_URL)
}
//获取二级分类
export const getC2 = (c1Id:number)=>request.get<any,CategoryResponseData>(API.GETC2_URL+c1Id);
//获取三级分类
export const getC3 = (c2Id:number)=>request.get<any,CategoryResponseData>(API.GETC3_URL+c2Id);

定义api类型 :src/api/product/type/category.ts

ts
//分类的数据的ts类型
export interface Category {
    id: number,
    name: string,
    category1Id?: number,
    category2Id?: number
}

//分类接口返回数据数据ts类型
export type CategoryResponseData = Category[];

封装属性数据api:src/api/product/attr.ts

ts
import request from '@/utils/request';
import type { AttrResponseData, Attr } from './type/attr';

//枚举地址
enum API {
    //获取平台属性与属性值GET:需要携带三个参数 1|2|3分类的ID
    GETATTR_URL="/admin/product/attrInfoList/",
    //添加新的属性|更新已有属性
    ADDORUPDATEATTR_URL="/admin/product/saveAttrInfo",
    //删除已有的属性接口
    DELETEATTR_URL="/admin/product/deleteAttr/"
}

//获取平台属性与属性值
export const reqAttrList = (c1Id:number|string,c2Id:number|string,c3Id:number|string)=>{
    return request.get<any,AttrResponseData>(API.GETATTR_URL+`${c1Id}/${c2Id}/${c3Id}`)
}

//添加属性与更新属性接口
export const reqAddOrUpdateAttr = (data:Attr)=>request.post<any,any>(API.ADDORUPDATEATTR_URL,data);

//删除已有的属性
export const reqDeleteAttr = (attrId:number)=>request.delete<any,any>(API.DELETEATTR_URL+attrId);

定义api类型:src/api/product/type/attr.ts

ts
//属性值的ts类型
export interface AttrValue {
    id?: number,
    valueName: string,
    attrId?: number,
    showInput?:boolean
}

// 属性值列表类型
export type AttrValueList = AttrValue[];

// 属性的类型
export interface Attr {
    id?: number,
    attrName: string,
    categoryId: number|string,
    categoryLevel: number|string,
    attrValueList: AttrValueList
}

// 属性列表类型
export type AttrResponseData = Attr[];

获取一级分类数据

category仓库 存储分类组件中的数据状态:src/store/category.ts

ts
//分类全局组件小仓库
import { defineStore } from "pinia";

//引入vue3组合式API函数
import { ref } from 'vue';

// 导入数据类型
import { CategoryResponseData } from "@/api/product/type/category";

//引入请求分类的API
import { getC1, getC2, getC3 } from '@/api/product/category';

// 创建切片 分类仓库
const useCategoryStore = defineStore('category',()=>{
    
    // 创建数据状态
    //一级分类的数据
    const c1Arr = ref<CategoryResponseData>([]);
    //二级分类的数据
    const c2Arr = ref<CategoryResponseData>([]);
    //三级分类的数据
    const c3Arr = ref<CategoryResponseData>([]);
    // 一级分类ID的字段
    const c1Id = ref<string | number>('');
    //二级分类的ID字段
    const c2Id = ref<string | number>('');
    //三级分类的ID字段
    const c3Id = ref<string | number>('');

    // 方法
    const getC1Data = async () => {
        const res:CategoryResponseData = await getC1()
        // 存储数据
        c1Arr.value = res
    }
    const getC2Data = async () => {
        const res:CategoryResponseData = await getC2(c1Id.value as number)
        // 存储数据
        c2Arr.value = res
    }
    const getC3Data = async () => {
        const res:CategoryResponseData = await getC3(c2Id.value as number)
        // 存储数据
        c3Arr.value = res
    }

    // 返回
    return {
        c1Arr,
        c2Arr,
        c3Arr,
        c1Id,
        c2Id,
        c3Id,
        getC1Data,
        getC2Data,
        getC3Data
    }
})
//对外暴露方法[默认暴露:引入的时候不需要花括号]
export default useCategoryStore;

分类组件中,挂载完成后,发送获取一级分类请求:src/components/Category/index.vue

ts
<template>
    <el-card>
        <!-- 设置行内 -->
        <el-form :inline="true">
            <el-form-item label="一级分类">
                <el-select >
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option label="Zone one" value="shanghai" />
                    <el-option label="Zone two" value="beijing" />
                </el-select>
            </el-form-item>
            
            <el-form-item label="二级分类">
                <el-select >
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option label="Zone one" value="shanghai" />
                    <el-option label="Zone two" value="beijing" />
                </el-select>
            </el-form-item>

            <el-form-item label="三级分类">
                <el-select >
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option label="Zone one" value="shanghai" />
                    <el-option label="Zone two" value="beijing" />
                </el-select>
            </el-form-item>

        </el-form>
    </el-card>
</template>

<script setup lang='ts'>
//为什么这里需要使用pinia仓库?(不用也可以)
//因为当前一级、二级、三级分类id,父组件获取属性需要,涉及到组件通信[自定义事件:子->父]、pinia
import useCategoryStore from "@/stores/category";
import { onMounted, onUnmounted } from "vue";

//获取分类的小仓库
const categoryStore = useCategoryStore()

// 生命周期
onMounted(()=>{
    //通知pinia调用getC1,获取一级分类的数据
    categoryStore.getC1Data()
})



</script>

<style scoped lang="less">
</style>

平台属性页面 :src/views/product/attr/index.vue

ts
<template>
    <div>
        <Category></Category>
        <el-card style="margin: 10px 0px">xxx</el-card>
    </div>
</template>

<script setup lang='ts'>

</script>

<style scoped lang="less">
</style>

获取二三级分类数据

src/components/Category/index.vue

ts
// 可以在 pinia 小仓库中使用watch监听 一级分类id 时发送请求(获取二级分类数据) 
// 或者在el-select 组件中发生 change 事件时 发送请求(获取二级分类数据) 

// 可以在 pinia 小仓库中使用watch监听 二级分类id 时发送请求(获取三级分类数据) 
// 或者在el-select 组件中发生 change 事件时 发送请求(获取三级分类数据) 


<template>
    <el-card>
        <!-- 设置行内 -->
        <el-form :inline="true">
            <el-form-item label="一级分类">
                <el-select v-model="categoryStore.c1Id" @change="handleChangeC1" >
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option :key="c1.id" v-for="(c1,index) in categoryStore.c1Arr" :label="c1.name" :value="c1.id"/>
                </el-select>
            </el-form-item>
            
            <el-form-item label="二级分类">
                <el-select v-model="categoryStore.c2Id" @change="handleChangeC2">
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option :key="c2.id" v-for="(c2,index) in categoryStore.c2Arr" :label="c2.name" :value="c2.id"/>
                </el-select>
            </el-form-item>

            <el-form-item label="三级分类">
                <el-select v-model="categoryStore.c3Id">
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option :key="c3.id" v-for="(c3,index) in categoryStore.c3Arr" :label="c3.name" :value="c3.id"/>
                </el-select>
            </el-form-item>

        </el-form>
    </el-card>
</template>

<script setup lang='ts'>
//为什么这里需要使用pinia仓库?(不用也可以)
//因为当前一级、二级、三级分类id,父组件获取属性需要,涉及到组件通信[自定义事件:子->父]、pinia
import useCategoryStore from "@/stores/category";
import { onMounted, onUnmounted } from "vue";

//获取分类的小仓库
const categoryStore = useCategoryStore()

// 生命周期
onMounted(()=>{
    //通知pinia调用getC1,获取一级分类的数据
    categoryStore.getC1Data()
})

// handle change事件回调
const handleChangeC1 = () => {
    //获取二级分类的数据
    categoryStore.getC2Data();
}
const handleChangeC2 = () => {
    //获取二级分类的数据
    categoryStore.getC3Data();
}

</script>

<style scoped lang="less">
</style>

清除上次数据

ts
// 一级分类在el-select 组件中发生 change 事件时 清除二级分类上次的c1Id 

// 一级分类在el-select 组件中发生 change 事件时 清除三级分类上次的c2Id和数组

// 二级分类在el-select 组件中发生 change 事件时 清除三级分类上次的c3Id 

// 销毁组件时(src/component/Category/index.vue),清除分类数组和id


// 生命周期
onMounted(()=>{
    //通知pinia调用getC1,获取一级分类的数据
    categoryStore.getC1Data()
})

//组件销毁钩子
onUnmounted(() => {
    //清空仓库全部的数据---只能是选择是API清空仓库数组,组合式API写法不行
    // categoryStore.$reset();
    categoryStore.c1Id = "";
    categoryStore.c2Id = "";
    categoryStore.c3Id = "";
    categoryStore.c1Arr = [];
    categoryStore.c2Arr = [];
    categoryStore.c3Arr = [];
});

// handle change事件回调
const handleChangeC1 = () => {

    //清除二级分类上一次收集的ID
    categoryStore.c2Id = "";
    //清除三级分类的ID与数组数据
    categoryStore.c3Id = "";
    categoryStore.c3Arr = [];
    //获取二级分类的数据
    categoryStore.getC2Data();

}
const handleChangeC2 = () => {
    //清除三级分类的ID
    categoryStore.c3Id = "";
    //获取二级分类的数据
    categoryStore.getC3Data();
}

平台属性表格数据渲染

静态页面

平台属性静态页面:src/views/product/attr/index.vue

vue
<template>
    <div>
        <Category></Category>
        <el-card style="margin: 10px 0px">
            <el-button type="primary" icon="Plus" >添加属性</el-button>
            <el-table border style="margin: 10px 0px" :data="attrList">
                <el-table-column type="index" align="center" label="序号" 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"
                            :type="item.id % 2 == 0 ? 'primary' : 'warning'"
                            v-for="(item, index) in row.attrValueList"
                            :key="item.id">
                                {{ item.valueName }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="120px">
                    <template #="{ row, $index }">
                        <el-button
                            type="warning"
                            size="small"
                            :icon="Edit"
                            ></el-button>
                        <el-popconfirm
                            :title="`你确定要删除${row.attrName}`"
                            width="250px"
                            >
                            <template #reference>
                                <el-button type="danger" size="small" :icon="Delete"></el-button>
                            </template>
                        </el-popconfirm>
                    </template>
                </el-table-column>
            </el-table>
        </el-card>
    </div>
</template>

<script setup lang='ts'>
import type { AttrResponseData } from "@/api/product/type/attr";
import useCategoryStore from "@/stores/category";
import { Plus, Edit, Delete } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { watch, ref, reactive, nextTick } from "vue";

// 定义数据状态
const attrList = ref<AttrResponseData>([])


</script>

<style scoped lang="less">
</style>

发送请求

发送请求(获取平台属性):src/views/product/attr/index.vue

ts
// 使用watch监听 仓库中三级分类id变化 且 仓库中三级分类id不为空 时发送请求

// 定义获取平台属性函数

<script setup lang='ts'>
import { reqAttrList } from "@/api/product/attr";
import type { AttrResponseData } from "@/api/product/type/attr";
import useCategoryStore from "@/stores/category";
import { Plus, Edit, Delete } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { watch, ref, reactive, nextTick } from "vue";

// 属性列表
const attrList = ref<AttrResponseData>([])

// 定义分类小仓库(分类组件的)
const categoryStore = useCategoryStore();

// 定义input组件实例
const inputRef = ref<any>();

// 方法
//获取全部已有的属性与属性值方法
const getAttrList = async () => {
    //一级、二级、三级分类的ID
    const { c1Id, c2Id, c3Id } = categoryStore;
    const result: AttrResponseData = await reqAttrList(c1Id, c2Id, c3Id)
    attrList.value = result;
}

// 监听
watch(()=>categoryStore.c3Id,()=>{
    //每一次监听到三级分类变化:把已有属性与属性值清空
    attrList.value = [];
    // 使用watch监听 仓库中三级分类id变化 且 仓库中三级分类id不为空 时发送请求
    categoryStore.c3Id && getAttrList();
})


</script>

渲染数据

渲染数据:src/views/product/attr/index.vue

vue
<template>
    <div>
        <Category></Category>
        <el-card style="margin: 10px 0px">
            <el-button type="primary" icon="Plus" >添加属性</el-button>
            <el-table border style="margin: 10px 0px" :data="attrList">
                <el-table-column type="index" align="center" label="序号" 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"
                            type="warning"
                            >{{ item.valueName }}</el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="120px">
                    <template #="{ row, $index }">
                        <el-button
                            type="warning"
                            size="small"
                            :icon="Edit"
                            ></el-button>
                        <el-popconfirm
                            :title="`你确定要删除${row.attrName}`"
                            width="250px"
                            >
                            <template #reference>
                                <el-button type="danger" size="small" :icon="Delete"></el-button>
                            </template>
                        </el-popconfirm>
                    </template>
                </el-table-column>
            </el-table>
        </el-card>
    </div>
</template>
vue
// 根据ID %2 动态的给 tag标签 设置type
< :type="item.id%2? 'primary':'danger'">

细节优化

添加属性按钮禁用:src/views/product/attr/index.vue

ts
<el-button type="primary" icon="Plus" :disabled="categoryStore.c3Id ? false : true">添加属性</el-button>

表格无数据时:src/views/product/attr/index.vue

vue
// 使用empty组件

<el-table border style="margin: 10px 0px" :data="attrList" v-show="attrList.length"></el-table>

<!-- 空数据时的组件 -->
<el-empty
    description="暂无数据"
    v-show="!attrList.length"
    :image="src"
    :image-size="400"
/>

过渡动画

过渡动画:src/views/product/attr/index.vue

vue
<transition name="attr"> <el-table></el-table> </transition>

<style scope>

.attr-enter-active,.attr-leave-active{
    transition: all 1.5s;
}
.attr-leave-to,.attr-enter{
    opacity: 0;
}

</style>

添加属性(场景切换)

静态页面

添加属性:src/views/product/attr/index.vue

vue
<template>
    <div>
        <Category></Category>
        <el-card style="margin: 10px 0px">
            <div v-show="scene === 0">
                <!-- 按钮 -->
                <el-button type="primary" icon="Plus" :disabled="categoryStore.c3Id ? false : true" @click="addAttr" >添加属性</el-button>
                <!-- 属性表格 -->
                <transition name="attr">
                    <el-table border style="margin: 10px 0px" :data="attrList" v-show="attrList.length">
                        <el-table-column type="index" align="center" label="序号" 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"
                                    type="warning"
                                    >{{ item.valueName }}</el-tag>
                            </template>
                        </el-table-column>
                        <el-table-column label="操作" width="120px">
                            <template #="{ row, $index }">
                                <el-button
                                    type="warning"
                                    size="small"
                                    :icon="Edit"
                                    ></el-button>
                                <el-popconfirm
                                    :title="`你确定要删除${row.attrName}`"
                                    width="250px"
                                    >
                                    <template #reference>
                                        <el-button type="danger" size="small" :icon="Delete"></el-button>
                                    </template>
                                </el-popconfirm>
                            </template>
                        </el-table-column>
                    </el-table>
                </transition>
                <!-- 空数据时的组件 -->
                <el-empty description="暂无数据"
                    v-show="!attrList.length"
                    :image="src"
                    :image-size="400"
                />
            </div>
            <!-- 添加属性 -->
            <div v-show="scene === 1">
                <el-form :inline="true">
                    <el-form-item label="属性名称">
                        <el-input
                            type="text"
                            placeholder="请你输入属性名称"
                        ></el-input>
                    </el-form-item>
                </el-form>
                <el-button type="primary" :icon="Plus">添加属性值</el-button>
                <el-button @click="scene = 0">取消</el-button>
                <el-table border style="margin: 10px 0px" >

                    <el-table-column
                        type="index"
                        label="序号"
                        align="center"
                        width="80px"
                    ></el-table-column>

                    <el-table-column label="属性值">
                        <!-- row:即为每一个属性值对象 -->
                        <template #="{ row, $index }">
                            <el-input
                            ref="inputRef"
                            size="small"
                            ></el-input>
                        </template>
                    </el-table-column>

                    <el-table-column label="操作">
                        <template #="{ row, $index }">
                            <el-button
                            type="danger"
                            :icon="Delete"
                            size="small"
                            ></el-button>
                        </template>
                    </el-table-column>
                </el-table>
                <el-button
                    type="primary"
                    >保存</el-button>
                <el-button @click="scene = 0">取消</el-button>
            </div>
        </el-card>
    </div>
</template>


<script>
const addAttr = () =>{
    // 设置场景为添加属性
    scene.value = 1;
}
</script>

细节优化

添加属性时 禁用分类切换:src/views/product/attr/index.vue父亲

ts
若禁用状态仍然可以选择下拉,需要升级element-plus版本

<Category :scene="scene"></Category>

添加属性时 禁用分类切换:src/components/Category/index.vue儿子

vue
<template>
    <el-card>
        <!-- 设置行内 -->
        <el-form :inline="true">
            <el-form-item label="一级分类">
                <el-select v-model="categoryStore.c1Id" @change="handleChangeC1" :disabled="scene==0?false:true" style="width: 180px">
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option :key="c1.id" v-for="(c1,index) in categoryStore.c1Arr" :label="c1.name" :value="c1.id"/>
                </el-select>
            </el-form-item>
            
            <el-form-item label="二级分类">
                <el-select v-model="categoryStore.c2Id" @change="handleChangeC2" :disabled="scene==0?false:true" style="width: 180px">
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option :key="c2.id" v-for="(c2,index) in categoryStore.c2Arr" :label="c2.name" :value="c2.id"/>
                </el-select>
            </el-form-item>

            <el-form-item label="三级分类">
                <el-select v-model="categoryStore.c3Id" :disabled="scene==0?false:true" style="width: 180px">
                    <!-- option:label 决定用户展示的选项 -->
                    <el-option :key="c3.id" v-for="(c3,index) in categoryStore.c3Arr" :label="c3.name" :value="c3.id"/>
                </el-select>
            </el-form-item>

        </el-form>
    </el-card>
</template>

<script setup lang='ts'>
//为什么这里需要使用pinia仓库?(不用也可以)
//因为当前一级、二级、三级分类id,父组件获取属性需要,涉及到组件通信[自定义事件:子->父]、pinia
import useCategoryStore from "@/stores/category";
import { onMounted, onUnmounted } from "vue";

// 接收props
defineProps(['scene'])
    
</<script>

封装接口

定义添加属性和更新属性接口

src/api/product/attr.ts

ts
//添加属性与更新属性接口
export const reqAddOrUpdateAttr = (data:Attr)=>request.post<any,any>(API.ADDORUPDATEATTR_URL,data);

//删除已有的属性
export const reqDeleteAttr = (attrId:number)=>request.delete<any,any>(API.DELETEATTR_URL+attrId);

src/api/product/type/attr.ts

ts
//属性值的ts类型
export interface AttrValue {
    id?: number,
    valueName: string,
    attrId?: number,
    showInput?:boolean
}

// 属性值列表类型
export type AttrValueList = AttrValue[];

// 属性的类型
export interface Attr {
    id?: number,
    attrName: string,
    categoryId: number|string,
    categoryLevel: number|string,
    attrValueList: AttrValueList
}

// 属性列表类型
export type AttrResponseData = Attr[];

收集表单数据

收集表单数据:src/views/product/attr/index.vue

ts
# 添加属性按钮的禁用时机

# 收集属性名称
	定义数据状态 attrParams 收集添加属性的属性和属性值
	双向绑定

# 收集input输入框的属性值
	定义数据状态 attrParams 收集添加属性的属性和属性值
	双向绑定

# 收集三级分类的id
	在添加属性按钮处触发
  	attrParams.categoryId = categoryStore.c3Id;

# 添加属性值绑定事件@click="addAttrValue":添加属性值 addAttrValue 往数组中push对象

# 添加属性值按钮设置禁用时机

# table渲染双向绑定数据 :date=attrParams.attrValueList
vue
<!-- 添加属性 -->
<div v-show="scene === 1">
    <el-form :inline="true">
        <el-form-item label="属性名称">
            <el-input
                type="text"
                placeholder="请你输入属性名称"
                v-model="attrParams.attrName"
            ></el-input>
        </el-form-item>
    </el-form>
    <el-button type="primary" :icon="Plus" @click="addAttrValue" :disabled="attrParams.attrName ? false : true">添加属性值</el-button>
    <el-button @click="scene = 0">取消</el-button>
    <el-table border style="margin: 10px 0px" :data="attrParams.attrValueList">
        <el-table-column
            type="index"
            label="序号"
            align="center"
            width="80px"
        ></el-table-column>
        <el-table-column label="属性值">
            <!-- row:即为每一个属性值对象 -->
            <template #="{ row, $index }">
                <el-input
                ref="inputRef"
                size="small"
                v-model="row.valueName"
                ></el-input>
            </template>
        </el-table-column>

        <el-table-column label="操作">
            <template #="{ row, $index }">
                <el-button
                type="danger"
                :icon="Delete"
                size="small"
                ></el-button>
            </template>
        </el-table-column>
    </el-table>
    <el-button
        type="primary"
        >保存</el-button>
    <el-button @click="scene = 0">取消</el-button>
</div>


<script>
// 添加属性和属性值,请求体
const attrParams = reactive<Attr>({
    categoryId: "", //新增的属性归属于哪一个三级分类
    categoryLevel: "3", //代表几级分类,默认3级
    attrName: "", //属性的名字
    //属性值数组
    attrValueList: [],
})

// 添加属性
const addAttr = () =>{


    //收集三级分类的ID[也可以点击保存的时候收集]
    attrParams.categoryId = categoryStore.c3Id;
    // 设置场景为添加属性
    scene.value = 1;
}

// 添加属性值
const addAttrValue =()=>{
    //向属性值数组添加属性对象
    //在每一个属性值对象身上放置编辑与查看模式标记
    attrParams.attrValueList.push({
        valueName: "",
        // showInput: true,
    })
    //当响应式数据发生变化后,立即调用nextTick方法,获取更新后的DOM|组件实例
    // nextTick(() => {
    //     inputRef.value.focus();
    // });
}

</script>

发送请求

src/views/product/attr/index.vue

ts
# 点击提交按钮发送请求

# 消息提示

# 回到场景0

# 重新获取属性数据

# 设置保存按钮的禁用时机
vue
<template>
	<el-button
        type="primary"
        @click="save"
        :disabled="attrParams.attrName && attrParams.attrValueList.length ? false : true"
        >保存</el-button>
    <el-button @click="scene = 0">取消</el-button>
</template>

<script>
// 提交
const save = async() =>{
    try {
        await reqAddOrUpdateAttr(attrParams)
        //消息提示
        ElMessage({
            type: "success",
            message: attrParams.id ? "更新成功" : "添加成功",
        });
        //切换场景为0
        scene.value = 0;
        //添加或者更新成功以后再次获取全部已有的属性
        getAttrList();
    } catch (error) {
        //消息提示
        ElMessage({
            type: "error",
            message: attrParams.id ? "更新失败" : "添加失败",
        });
    }
    
}
</script>
	
<style>

</style>

清空数据

src/views/product/attr/index.vue

vue
# 在添加属性按钮处,调用清空数据方法

<script>
// 清空数据方法
const reset =()=>{
	// 使用Object.assign()方法进行合并,相同的字段会合并,不同的字段会合并
}
</script>
vue
<template>

</template>

<script>
// 添加属性
const addAttr = () =>{

    //清空数据
    reset();
    //收集三级分类的ID[也可以点击保存的时候收集]
    attrParams.categoryId = categoryStore.c3Id;
    // 设置场景为添加属性
    scene.value = 1;
}    
    
// 用户取消、保存成功以后要清空数据的方法
const reset = () => {
  //ES6的语法:Object.assign
  Object.assign(attrParams, {
    categoryId: "", //新增的属性归属于哪一个三级分类
    categoryLevel: "3", //代表几级分类
    attrName: "", //属性的名字
    //属性值数组
    attrValueList: [],
    // 更新时
    id: "",
  });
};
</script>

编辑与查看切换

src/views/product/attr/index.vue

谁显示谁隐藏实现方式:第一种方式使用v-if或者v-show,第二种使用行内样式display:none,block

ts
# 增加div静态页面

# 定义数据状态:控制input和div标签的显示和隐藏
	定义在attrValueList 数组中,在添加属性值的回调函数中,往数组中添加元素对象时,对象中添加 showInput属性。

# 给input绑定blur事件

# 给div绑定点击事件

# 解决全部同时显示与隐藏
	在 attrValueList 数组的 元素对象中 添加 input标记

# 解决div空白的时候,无法切换
  	属性值不能为空,为空时 在数组中删除,并停止
  
# 属性值不能重复
	在blur时,触发
	因为是双向绑定的,在添加时,自己已经在数组中,把自己去除之后(使用 trim()方法去除空格),用剩余的数组再去使用find()方法,判断find()的返回值是否存在,存在即有重复,把重复的数据从数组中删除。
vue
<template>
<el-table-column label="属性值">
    <!-- row:即为每一个属性值对象 -->
    <template #="{ row, $index }">
        <!-- input标签 -->
        <el-input
        ref="inputRef"
        size="small"
        v-if="row.showInput"
        v-model="row.valueName"
        @blur="toLook(row, $index)"
        ></el-input>
        <!-- div标签 -->
        <div v-else @click="toEdit(row)">{{ row.valueName }}</div>
    </template>
</el-table-column>

</template>

<script>
// input失去焦点
const toLook = (row:AttrValue,index:number) => {

    //属性值不能为空
    if (row.valueName.trim() == "") {
        ElMessage({
            type: "error",
            message: "属性值不能为空",
        });
        //从数组中删除当前元素
        attrParams.attrValueList.splice(index, 1);
        //属性值为空,后面的语句不在执行,div不出现
        return;
    }

    //判断属性值不能重复
    const repeat = attrParams.attrValueList.find((item) => {
        //把除自己以外的元素都返回
        if (row != item) {
        return row.valueName.trim() === item.valueName.trim();
        }
    })
    if (repeat) {
        ElMessage({
        type: "error",
        message: "属性值不能重复",
        });
        //从数组中删除当前元素
        attrParams.attrValueList.splice(index, 1);
        return;
    }

    //显示div
    row.showInput = false
}

//div点击事件
const toEdit = (row: AttrValue) => {
    //点击div 变为 input
    row.showInput = true;
    // input输入框获取焦点
    nextTick(() => {
        inputRef.value.focus();
    });
};
</script>

编辑与查看切换时聚焦

查看element-plus文件中的input组件的聚焦方法

ts
# 获取input 对象

# 在添加属性按钮时,添加input focus方法
   在响应式数据发生变化后,立即调用nextTick方法,获取dom元素

# 在点击div事件时,添加input focus方法    
   在响应式数据发生变化后,立即调用nextTick方法,获取dom元素
vue
<template>

</template>

<script>
// 添加属性值
const addAttrValue =()=>{
    //向属性值数组添加属性对象
    //在每一个属性值对象身上放置编辑与查看模式标记
    attrParams.attrValueList.push({
        valueName: "",
        showInput: true,
    })
    //当响应式数据发生变化后,立即调用nextTick方法,获取更新后的DOM|组件实例
    nextTick(() => {
        inputRef.value.focus();
    });
}
</script>

保存按钮禁用

ts
# 在 attrName存在 且 attrValueList数组的 长度>0 时 不禁用
vue
<template>
<el-button
    type="primary"
    @click="save"
    :disabled="attrParams.attrName && attrParams.attrValueList.length ? false : true"
    >保存</el-button>
<el-button @click="scene = 0">取消</el-button>
</template>

<script>

</script>

删除按钮

ts
# 删除按钮
	在删除按钮设置点击事件,把当前属性值从 数组中 删除(不用发送请求)
vue
<template>
<el-table-column label="操作">
    <template #="{ row, $index }">
        <el-button
        type="danger"
        :icon="Delete"
        size="small"
        @click="attrParams.attrValueList.splice($index, 1)"
        ></el-button>
    </template>
</el-table-column>
</template>

<script>

</script>

更新属性值

收集数据

ts
# 把当前行对象中的数据 赋值给 attrparams
Object.assign(attrParams,row)(存在浅拷贝的问题)

# 在 reset清空数据 时,把ID也清空

# 取消按钮时,attrParams的数据会同步到attrValueList数组中
	原因:引用数据类型浅拷贝的问题
    解决:使用json深拷贝
    Object.assign(attrParams,JSON.parse(JSON.stringify(row)))
vue
<template>
<el-table-column label="操作" width="120px">
    <template #="{ row, $index }">
        <el-button
            type="warning"
            size="small"
            :icon="Edit"
            @click="updateAttr(row)"
            >
        </el-button>
        <el-popconfirm
            :title="`你确定要删除${row.attrName}`"
            width="250px"
            >
            <template #reference>
                <el-button type="danger" size="small" :icon="Delete"></el-button>
            </template>
        </el-popconfirm>
    </template>
</el-table-column>
</template>

<script>
// 更新已有属性
const updateAttr = (row:Attr) =>{
    // 获取当前点击行对象,使用lodash插件中的函数,深拷贝
    Object.assign(attrParams, cloneDeep(row));
    // 切换场景为1
    scene.value = 1
}
</script>

使用lodash解决浅拷贝问题

ts
# 使用lodash(项目中已包含)防抖节流中的递归函数,解决对象的浅拷贝问题,把浅拷贝转为深拷贝

# 在入口文件中引入:main.ts
	import cloneDeep from 'lodash/cloneDeep'
# 取消按钮时,attrParams的数据会同步到attrValueList数组中
	解决:使用递归函数
	Object.assign(attrParams,cloneDeep(row))

删除属性

封装接口

项目代码

src/api/product/attr.ts

ts
//删除已有的属性
export const reqDeleteAttr = (attrId:number)=>request.delete<any,any>(API.DELETEATTR_URL+attrId);

绑定事件发送请求

在组件中绑定单机事件发送删除属性的请求:src/views/product/attr/index.vue

ts
# 删除成功后,重新获取属性列表
vue
<template>
<el-popconfirm
    :title="`你确定要删除${row.attrName}`"
    width="250px"
    @confirm="deleteAttr(row.id)"
    >
    <template #reference>
        <el-button type="danger" size="small" :icon="Delete"></el-button>
    </template>
</el-popconfirm>
</template>

<script>
//删除已有的属性
const deleteAttr = async (id: number) => {
    try {
        //删除已有属性成功
        await reqDeleteAttr(id);
        //消息提示
        ElMessage({
            type:'success',
            message:'删除成功'
        });
        //再次获取最新剩下全部属性
        getAttrList();
    } catch (error) {
        console.log(error)
    }
}
</script>