五、项目搭建-商品管理(品牌,属性)

89 阅读2分钟

商品管理

1、品牌管理

功能分析:

image-20230902223658080.png

点击添加/修改品牌 显示如下:

image-20230902230254914.png

1.1 品牌管理模块静态搭建
  1. 去除layout > index.vue 中的 .layout_main 区域的 背景色。
........ 
.layout_main {
    position: absolute;
    width: calc(100% - $base-menu-width);
    height: calc(100vh - $base-tabbar-height);
    left: $base-menu-width;
    top: $base-tabbar-height;
    padding: 20px;
    overflow: auto;
    transition: all 0.3s;
    // background: #92dd22; +++++++++++=
    &.fold {
      width: calc(100vw - $base-menu-min-width);
      left: $base-menu-min-width;
    }
  }
..........
  1. 编辑品牌管理页面 views > product > trademark > index.vue
<template>
  <div>
     <el-card class="box-card">
​
            <!-- 卡片顶部添加品牌按钮 -->
            <el-button type="primary" size="default" icon="Plus" @click="addTrademark" v-has="`btn.Trademark.add`">添加品牌</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 
                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>
     
  </div>
</template>
<script setup lang="ts">
import {ref,reactive} from "vue"//添加品牌按钮的回调
const addTrademark = () => {
  console.log("添加品牌方法被触发; 。。。。。")
}
​
</script>
<style lang="scss" scoped>
</style>
1.2 品牌管理模块数据展示

静态搭建完成后,现在需要请求真实的数据来填充。

  1. 在 api > product 下新建好对应左侧菜单栏下对应的接口文件夹 trademark attr sku spu

  2. 在 api > product > trademark 下新建 index. ts

    //书写品牌管理模块接口
    import request from "@/utils/request";
    ​
    //品牌管理模块接口地址
    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, any>(API.TRADEMARK_URL + `${page}/${limit}`);
    ​
    ​
    
  3. 在 views > product > trademark 的index.vue 中调用接口获取数据

<template>
  <div>
     <el-card class="box-card">
​
            <!-- 卡片顶部添加品牌按钮 -->
            <el-button type="primary" size="default" icon="Plus" @click="addTrademark" v-has="`btn.Trademark.add`">添加品牌</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, $index }">
                        <img :src="row.logoUrl" style="width:100px;height: 100px;">
                    </template>
                </el-table-column>
                <el-table-column label="品牌操作">
                    <template #="{ row, $index }">
                        <el-button type="primary" size="small" icon="Edit" @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 
                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>
     
  </div>
</template>
<script setup lang="ts">
import {ref,reactive,onMounted} from "vue"import { reqHasTrademark, reqAddOrUpdateTrademark, reqDeleteTrademark } from '@/api/product/trademark';
​
//当前页码
let pageNo = ref<number>(1);
//每一页展示多少条数据
let limit = ref<number>(3);
//存储已有品牌数据总数
let total = ref<number>(0);
//存储已有品牌的数据
let trademarkArr = ref<Records>([]);
​
​
//获取已有品牌的接口封装为一个函数:在任何情况下向获取数据,调用次函数即可
const getHasTrademark = async (pager = 1) => {
    //当前页码
    pageNo.value = pager;
    let result = await reqHasTrademark(pageNo.value, limit.value);
    console.log("获取到的结果========:", result)
    if (result.code == 200) {
        //存储已有品牌总个数
        total.value = result.data.total;
        trademarkArr.value = result.data.records;
    }
}
​
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
    getHasTrademark();
});
​
//添加品牌按钮的回调
const addTrademark = () => {
  console.log("添加品牌方法被触发; 。。。。。")
}
​
//修改已有品牌的按钮的回调
//row:row即为当前已有的品牌
const updateTrademark = (row: TradeMark) => {
    //清空校验规则错误提示信息
   console.log("编辑品牌方法被触发; 。。。。。")
}
​
//对话框底部取消按钮
const cancel = () => {
    //对话框隐藏
     console.log("取消对话框 。。。。。")
}
​
const removeTradeMark = async (id: number) => {
    //点击确定按钮删除已有品牌请求
     console.log("删除品牌方法被触发; 。。。。。")
}
</script>
<style lang="scss" scoped></style>
1.3 已有品牌ts类型

上一步的请求和响应的数据都是any,需要给请求和响应的数据定义type.

  1. 在 api > product > trademark 下新建 type.ts
export interface ResponseData {
    code: number,
    message: string,
    ok: boolean
}
​
//已有的品牌的ts数据类型
export interface TradeMark {
    id?: number, // 品牌编辑和新增是同一个,当新增是时候id可能没有所以用 id?
    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
    }
}
  1. 在 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}`);
​
​
  1. 在views > procduct > trademark > index.vue 中引入
..............................
<script setup lang="ts">
import {ref,reactive,onMounted} 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>([]);
​
​
//获取已有品牌的接口封装为一个函数:在任何情况下向获取数据,调用次函数即可
const getHasTrademark = async (pager = 1) => {
    //当前页码
    pageNo.value = pager;
  // ++++++++++++++++++++++++++
    let result:TradeMarkResponseData  = await reqHasTrademark(pageNo.value, limit.value);
    if (result.code == 200) {
        //存储已有品牌总个数
        total.value = result.data.total;
        trademarkArr.value = result.data.records;
    }
}
​
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
    getHasTrademark();
});
​
....................
​
1.4 品牌管理分页展示数据

实现分析:在点击分页上的页码和左右箭头以及输入的跳转页的时候都应该刷新数据。

在views > procduct > trademark > index.vue

<template>
  <div>
     <el-card class="box-card">
​
            <!-- 卡片顶部添加品牌按钮 -->
            <el-button type="primary" size="default" icon="Plus" @click="addTrademark" v-has="`btn.Trademark.add`">添加品牌</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, $index }">
                        <img :src="row.logoUrl" style="width:100px;height: 100px;">
                    </template>
                </el-table-column>
                <el-table-column label="品牌操作">
                    <template #="{ row, $index }">
                        <el-button type="primary" size="small" icon="Edit" @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:可以设置分页器六个子组件布局调整
                   pager-count :分页组件显示多少个页数按钮,超过这个数则省略号省略
            -->
            <el-pagination 
                @size-change="sizeChange"  <!-- ++++++++++++ -->
                @current-change="getHasTrademark"  <!-- ++++++++++++ -->
                :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>
     
  </div>
</template>
<script setup lang="ts">
import {ref,reactive,onMounted} 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>([]);
​
​
//获取已有品牌的接口封装为一个函数:在任何情况下向获取数据,调用次函数即可
const getHasTrademark = async (pager = 1) => {
    //当前页码 (默认给的就是1,为了后面改变页容量的时候让其从第一页开始)
    pageNo.value = pager;
    let result:TradeMarkResponseData  = await reqHasTrademark(pageNo.value, limit.value);
    if (result.code == 200) {
        //存储已有品牌总个数
        total.value = result.data.total;
        trademarkArr.value = result.data.records;
    }
}
​
//当下拉菜单发生变化的时候触发次方法 +++++++++++++++
//这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
    //当前每一页的数据量发生变化的时候,当前页码归1
    getHasTrademark();
}
​
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
    getHasTrademark();
});
​
...................
​
1.5 新增品牌弹框静态搭建

功能分析: 点击添加品牌或修改时弹框显示,弹框中有品牌名称输入框和logo上传

  1. 在views > procduct > trademark > index.vue 中添加弹框组件代码,和添加 添加品牌和修改品牌的点击方法
<template>
  <div>
     <el-card class="box-card">
​
            <!-- 卡片顶部添加品牌按钮 -->
            <el-button type="primary" size="default" icon="Plus" @click="addTrademark" v-has="`btn.Trademark.add`">添加品牌</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, $index }">
                        <img :src="row.logoUrl" style="width:100px;height: 100px;">
                    </template>
                </el-table-column>
                <el-table-column label="品牌操作">
                    <template #="{ row, $index }">
                        <el-button type="primary" size="small" icon="Edit" @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:可以设置分页器六个子组件布局调整
                   pager-count :分页组件显示多少个页数按钮,超过这个数则省略号省略
            -->
            <el-pagination 
                @size-change="sizeChange"
                @current-change="getHasTrademark" 
                :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" 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 {ref,reactive,onMounted,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 (pager = 1) => {
    //当前页码 (默认给的就是1,为了后面改变页容量的时候让其从第一页开始)
    pageNo.value = pager;
    let result:TradeMarkResponseData  = await reqHasTrademark(pageNo.value, limit.value);
    if (result.code == 200) {
        //存储已有品牌总个数
        total.value = result.data.total;
        trademarkArr.value = result.data.records;
    }
}
​
//当下拉菜单发生变化的时候触发次方法
//这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
    //当前每一页的数据量发生变化的时候,当前页码归1
    getHasTrademark();
}
​
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
    getHasTrademark();
});
​
//添加品牌按钮的回调
const addTrademark = () => {
  console.log("添加品牌方法被触发; 。。。。。")
   //对话框显示
    dialogFormVisible.value = true;
    //清空收集数据
    trademarkParams.id = 0;
    trademarkParams.tmName = '';
    trademarkParams.logoUrl = '';
    //第一种写法:ts的问号语法
    // formRef.value?.clearValidate('tmName');
    // formRef.value?.clearValidate('logoUrl');
    nextTick(() => {
        formRef.value.clearValidate('tmName');
        formRef.value.clearValidate('logoUrl');
    })
}
​
//修改已有品牌的按钮的回调
//row:row即为当前已有的品牌
const updateTrademark = (row: TradeMark) => {
    //清空校验规则错误提示信息
   console.log("编辑品牌方法被触发; 。。。。。")
    //清空校验规则错误提示信息
    nextTick(() => {
        formRef.value.clearValidate('tmName');
        formRef.value.clearValidate('logoUrl');
    })
    //对话框显示
    dialogFormVisible.value = true;
    //ES6语法合并对象
    Object.assign(trademarkParams, row);
}
​
//对话框底部取消按钮
const cancel = () => {
    //对话框隐藏
     console.log("取消对话框 。。。。。")
      //对话框隐藏
    dialogFormVisible.value = false;
}
​
const removeTradeMark = async (id: number) => {
    //点击确定按钮删除已有品牌请求
     console.log("删除品牌方法被触发; 。。。。。")
}
​
//上传图片组件->上传图片之前触发的钩子函数
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) => {
    console.log("图片上传成功后的回调 。。。。。")
​
}
​
</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>
1.6 收集新增品牌数据
  1. 在 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);
    }
}
  1. project\src\views\product\trademark\index.vue 中实现新增或者修改
<template>
  <div>
    ..............
​
      <!-- 对话框组件:在添加品牌与修改已有品牌的业务时候使用结构 -->
        <!-- 
            v-model:属性用户控制对话框的显示与隐藏的 true显示 false隐藏
             title:设置对话框左上角标题
        -->
        <el-dialog v-model="dialogFormVisible" :title="trademarkParams.id ? '修改品牌' : '添加品牌'">
            <el-form style="width: 80%;" :model="trademarkParams" 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 {ref,reactive,onMounted,nextTick} from "vue"
  // +++++++++++++++++++++
import { ElMessage, UploadProps, formEmits } from 'element-plus'
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 (pager = 1) => {
    //当前页码 (默认给的就是1,为了后面改变页容量的时候让其从第一页开始)
    pageNo.value = pager;
    let result:TradeMarkResponseData  = await reqHasTrademark(pageNo.value, limit.value);
    if (result.code == 200) {
        //存储已有品牌总个数
        total.value = result.data.total;
        trademarkArr.value = result.data.records;
    }
}
​
//当下拉菜单发生变化的时候触发次方法
//这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
    //当前每一页的数据量发生变化的时候,当前页码归1
    getHasTrademark();
}
​
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
    getHasTrademark();
});
​
//添加品牌按钮的回调
const addTrademark = () => {
  console.log("添加品牌方法被触发; 。。。。。")
   //对话框显示
    dialogFormVisible.value = true;
    //清空收集数据
    trademarkParams.id = 0;
    trademarkParams.tmName = '';
    trademarkParams.logoUrl = '';
    //第一种写法:ts的问号语法
    // formRef.value?.clearValidate('tmName');
    // formRef.value?.clearValidate('logoUrl');
    nextTick(() => {
        formRef.value.clearValidate('tmName');
        formRef.value.clearValidate('logoUrl');
    })
}
​
//修改已有品牌的按钮的回调
//row:row即为当前已有的品牌
const updateTrademark = (row: TradeMark) => {
    //清空校验规则错误提示信息
   console.log("编辑品牌方法被触发; 。。。。。")
    //清空校验规则错误提示信息
    nextTick(() => {
        formRef.value.clearValidate('tmName');
        formRef.value.clearValidate('logoUrl');
    })
    //对话框显示
    dialogFormVisible.value = true;
    //ES6语法合并对象
    Object.assign(trademarkParams, row);
}
​
//对话框底部取消按钮
const cancel = () => {
    //对话框隐藏
     console.log("取消对话框 。。。。。")
      //对话框隐藏
    dialogFormVisible.value = false;
}
​
const removeTradeMark = async (id: number) => {
    //点击确定按钮删除已有品牌请求
     console.log("删除品牌方法被触发; 。。。。。")
}
​
//上传图片组件->上传图片之前触发的钩子函数 +++++++++++===
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) => {
    console.log("图片上传成功后的回调 。。。。。")
    //response:即为当前这次上传图片post请求服务器返回的数据
    //收集上传图片的地址,添加一个新的品牌的时候带给服务器
    trademarkParams.logoUrl = response.data;
    //图片上传成功,清除掉对应图片校验结果
    formRef.value.clearValidate('logoUrl');
}
​
</script>
..........
​
1.7 完成添加新的品牌的业务
<template>
  <div>
     ................
      <!-- 对话框组件:在添加品牌与修改已有品牌的业务时候使用结构 -->
        <!-- 
            v-model:属性用户控制对话框的显示与隐藏的 true显示 false隐藏
             title:设置对话框左上角标题
        -->
        <el-dialog v-model="dialogFormVisible" :title="trademarkParams.id ? '修改品牌' : '添加品牌'">
            <el-form style="width: 80%;" :model="trademarkParams" 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 {ref,reactive,onMounted,nextTick} from "vue"
import { ElMessage, UploadProps, formEmits } from 'element-plus'
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 (pager = 1) => {
    //当前页码 (默认给的就是1,为了后面改变页容量的时候让其从第一页开始)
    pageNo.value = pager;
    let result:TradeMarkResponseData  = await reqHasTrademark(pageNo.value, limit.value);
    if (result.code == 200) {
        //存储已有品牌总个数
        total.value = result.data.total;
        trademarkArr.value = result.data.records;
    }
}
​
//当下拉菜单发生变化的时候触发次方法
//这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
    //当前每一页的数据量发生变化的时候,当前页码归1
    getHasTrademark();
}
​
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
    getHasTrademark();
});
​
//添加品牌按钮的回调
const addTrademark = () => {
  console.log("添加品牌方法被触发; 。。。。。")
   //对话框显示
    dialogFormVisible.value = true;
    //清空收集数据
    trademarkParams.id = 0;
    trademarkParams.tmName = '';
    trademarkParams.logoUrl = '';
    //第一种写法:ts的问号语法
    // formRef.value?.clearValidate('tmName');
    // formRef.value?.clearValidate('logoUrl');
    nextTick(() => {
        formRef.value.clearValidate('tmName');
        formRef.value.clearValidate('logoUrl');
    })
}
​
//修改已有品牌的按钮的回调
//row:row即为当前已有的品牌
const updateTrademark = (row: TradeMark) => {
    //清空校验规则错误提示信息
   console.log("编辑品牌方法被触发; 。。。。。")
    //清空校验规则错误提示信息
    nextTick(() => {
        formRef.value.clearValidate('tmName');
        formRef.value.clearValidate('logoUrl');
    })
    //对话框显示
    dialogFormVisible.value = true;
    //ES6语法合并对象
    Object.assign(trademarkParams, row);
}
​
//对话框底部取消按钮
const cancel = () => {
    //对话框隐藏
     console.log("取消对话框 。。。。。")
      //对话框隐藏
    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;
    }
}
​
............
​
1.8完成修改已有品牌的业务

上一步已经完成

1.9 品牌管理模块表单校验

功能分析:实现添加品牌和修改的弹窗参数校验,使用element-ui的校验参数组件

<template>
  <div>
 。...........................
​
      <!-- 对话框组件:在添加品牌与修改已有品牌的业务时候使用结构 -->
        <!-- 
            v-model:属性用户控制对话框的显示与隐藏的 true显示 false隐藏
             title:设置对话框左上角标题
        -->
        <el-dialog v-model="dialogFormVisible" :title="trademarkParams.id ? '修改品牌' : '添加品牌'">
          <!-- el-form 加上rules,要校验的属性上加上prop +++++++++++++++++++++++ -->
            <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 {ref,reactive,onMounted,nextTick} from "vue"
import { ElMessage, UploadProps, formEmits } from 'element-plus'
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 (pager = 1) => {
    //当前页码 (默认给的就是1,为了后面改变页容量的时候让其从第一页开始)
    pageNo.value = pager;
    let result:TradeMarkResponseData  = await reqHasTrademark(pageNo.value, limit.value);
    if (result.code == 200) {
        //存储已有品牌总个数
        total.value = result.data.total;
        trademarkArr.value = result.data.records;
    }
}
​
//当下拉菜单发生变化的时候触发次方法
//这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
    //当前每一页的数据量发生变化的时候,当前页码归1
    getHasTrademark();
}
​
//组件挂载完毕钩子---发一次请求,获取第一页、一页三个已有品牌数据
onMounted(() => {
    getHasTrademark();
});
​
//添加品牌按钮的回调
const addTrademark = () => {
  console.log("添加品牌方法被触发; 。。。。。")
   //对话框显示
    dialogFormVisible.value = true;
    //清空收集数据
    trademarkParams.id = 0;
    trademarkParams.tmName = '';
    trademarkParams.logoUrl = '';
    //第一种写法:ts的问号语法
    // formRef.value?.clearValidate('tmName');
    // formRef.value?.clearValidate('logoUrl');
    nextTick(() => {
        formRef.value.clearValidate('tmName');
        formRef.value.clearValidate('logoUrl');
    })
}
​
//修改已有品牌的按钮的回调
//row:row即为当前已有的品牌
const updateTrademark = (row: TradeMark) => {
    //清空校验规则错误提示信息
   console.log("编辑品牌方法被触发; 。。。。。")
    //清空校验规则错误提示信息
    nextTick(() => {
        formRef.value.clearValidate('tmName');
        formRef.value.clearValidate('logoUrl');
    })
    //对话框显示
    dialogFormVisible.value = true;
    //ES6语法合并对象
    Object.assign(trademarkParams, row);
}
​
//对话框底部取消按钮
const cancel = () => {
    //对话框隐藏
     console.log("取消对话框 。。。。。")
      //对话框隐藏
    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 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 }
    ]
}
​
................................
​
2.0 品牌管理模块删除业务
  1. 在 project\src\api\product\trademark\index.ts 下新增删除的方法

    ...........
    //添加与修改已有品牌接口方法
    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. project\src\views\product\trademark\index.vue 中实现删除逻辑

    <template>
      <div>
       ...............
                        <template #="{ row, $index }">
                            <el-button type="primary" 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>
              ....................
     
         
      </div>
    </template>
    <script setup lang="ts">
    import {ref,reactive,onMounted,nextTick} from "vue"
    import { ElMessage, UploadProps, formEmits } from 'element-plus'
    import { reqHasTrademark, reqAddOrUpdateTrademark, reqDeleteTrademark } from '@/api/product/trademark';
      // 引入删除api  +++++++++++++++++++++
    import type { Records, TradeMarkResponseData, TradeMark } from '@/api/product/trademark/type'
     ...................
       
    //气泡确认框确定按钮的回调 +++++++++++++++++++++++
    const removeTradeMark = async (id: number) => {
        //点击确定按钮删除已有品牌请求
         console.log("删除品牌方法被触发; 。。。。。")
           //点击确定按钮删除已有品牌请求
        let result = await reqDeleteTrademark(id);
        if (result.code == 200) {
            //删除成功提示信息
            ElMessage({
                type: 'success',
                message: '删除品牌成功'
            });
            //再次获取已有的品牌数据
            getHasTrademark(trademarkArr.value.length > 1 ? pageNo.value : pageNo.value - 1);
        } else {
            ElMessage({
                type: 'error',
                message: '删除品牌失败'
            })
        }
    }
    ​
    ............
    ​
    

2、属性管理

2.1 平台属性管理模块静态搭建

功能实现分析:

image-20230827161540219.png

image-20230827162237852.png

点击添加属性按钮弹窗如下:

image-20230902230647998.png

  1. 抽取左侧菜单栏中商品管理下的 spu和属性管理的头部组件抽取。

    新建公共组件\src\components\Category\index.vue

    <template>
      
     <el-card>
            <el-form :inline="true">
                <el-form-item label="一级分类">
                    <el-select>
                        <el-option label="北京"></el-option>
                        <el-option label="上海"></el-option>
                        <el-option label="广州"></el-option>
                        <el-option label="深圳"></el-option>
                    </el-select>
                </el-form-item>
               <el-form-item label="二级分类">
                    <el-select>
                        <el-option label="北京"></el-option>
                        <el-option label="上海"></el-option>
                        <el-option label="广州"></el-option>
                        <el-option label="深圳"></el-option>
                    </el-select>
                </el-form-item>
                <el-form-item label="三级分类">
                    <el-select>
                        <el-option label="北京"></el-option>
                        <el-option label="上海"></el-option>
                        <el-option label="广州"></el-option>
                        <el-option label="深圳"></el-option>
                    </el-select>
                </el-form-item>
            </el-form>
        </el-card></template>
    <script setup lang="ts">
    import {ref,reactive} from "vue"</script>
    <style lang="scss" scoped></style>
    1. 在project\src\components\index.ts中注册全局组件

      //引入项目中全部的全局组件
      import SvgIcon from './SvgIcon/index.vue';
      // ++++++++++++
      import Category from './Category/index.vue';
      //引入element-plus提供全部图标组件
      import * as ElementPlusIconsVue from '@element-plus/icons-vue'
      //全局对象 ++++++++++++++++
      const allGloablComponent: any = { SvgIcon,Category };
      //对外暴露插件对象
      export default {
          //务必叫做install方法
          install(app: any) {
              //注册项目全部的全局组件
              Object.keys(allGloablComponent).forEach(key => {
                  //注册为全局组件
                  app.component(key, allGloablComponent[key]);
              });
              //将element-plus提供图标注册为全局组件
              for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
                  app.component(key, component)
              }
          }
      }
      
  1. 编辑 project\src\views\product\attr\index.vue

    <template>
      <div>
        <!-- 三级分类全局组件 -->
        <Category />
        <el-card style="margin: 10px 0px">
          <el-button type="primary" size="default" icon="Plus">添加属性</el-button>
          <el-table border style="margin: 10px 0px">
            <el-table-column label="序号" type="index" align="center" width="80px" ></el-table-column>
            <el-table-column label="属性名称" width="120px" ></el-table-column>
            <el-table-column label="属性值名称"></el-table-column>
            <el-table-column label="操作" width="120px"></el-table-column>
          </el-table>
        </el-card>
      </div>
    </template>
    <script setup lang="ts">
    import { ref, reactive } from 'vue'
    </script>
    <style lang="scss" scoped></style>
    1. 静态效果展示

image-20230827164757558.png

2.2 一级分类数据展示与收集
  1. 定义接口和商品仓库中所用到的数据类型

    1. 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[]
}
​
//属性与属性值的ts类型//属性值对象的ts类型
export interface AttrValue {
    id?: number,
    valueName: string,
    attrId?: number,
    flag?:boolean
​
}
//存储每一个属性值的数组类型
export type AttrValueList = AttrValue[];
//属性对象
export interface Attr {
    id?: number,
    attrName: string,
    categoryId: number|string,
    categoryLevel: number,
    attrValueList: AttrValueList
}
//存储每一个属性对象的数组ts类型
export type AttrList = Attr[];
//属性接口返回的数据ts类型
export interface AttrResponseData extends ResponseData {
    data: Attr[]
}
  1. project\src\store\modules\types\type.ts

    import type { RouteRecordRaw } from "vue-router";
    //定义小仓库数据state类型
    export interface UserState {
        token: string | null;
        menuRoutes: RouteRecordRaw[],
        username: '',
        avatar: '',
        buttons:string[]
    }
    ​
    //定义分类仓库state对象的ts类型
    export interface CategoryState {
        c1Id: string | number,
        c1Arr: CategoryObj[],
        c2Arr: CategoryObj[],
        c2Id: string | number,
        c3Arr: CategoryObj[],
        c3Id: string | number
    }
    
  1. 在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)
  1. 在 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";
let useCategoryStore = defineStore('Category', {
    state: (): CategoryState => {
        return {
            //存储一级分类的数据
            c1Arr: [],
            //存储一级分类的ID
            c1Id: '',
            //存储对应一级分类下二级分类的数据
            c2Arr: [],
            //收集二级分类的ID
            c2Id: '',
            //存储三级分类的数据
            c3Arr: [],
            //存储三级分类的ID
            c3Id: ''
        }
    },
    actions: {
        //获取一级分类的方法
        async getC1() {
            //发请求获取一级分类的数据
            let result: CategoryResponseData = await reqC1();
            if (result.code == 200) {
                this.c1Arr = result.data;
            }
        },
        //获取二级分类的数据
        async getC2() {
            //获取对应一级分类的下二级分类的数据
            let result: CategoryResponseData = await reqC2(this.c1Id);
            if (result.code == 200) {
                this.c2Arr = result.data;
            }
        },
        //获取三级分类的数据
        async getC3() {
            let result: CategoryResponseData = await reqC3(this.c2Id);
            if (result.code == 200) {
                this.c3Arr = result.data;
            }
        }
    },
    getters: {
​
    }
​
})
​
export default useCategoryStore;
  1. 编辑 project\src\components\Category\index.vue
<template>
    <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>
</template><script setup lang="ts">
//引入组件挂载完毕方法
import { onMounted } from 'vue';
//引入分类相关的仓库
import useCategoryStore from '@/store/modules/category';
let categoryStore = useCategoryStore();
//分类全局组件挂载完毕,通知仓库发请求获取一级分类的数据
onMounted(() => {
    getC1();
});
//通知仓库获取一级分类的方法
const getC1 = () => {
    //通知分类仓库发请求获取一级分类的数据
    categoryStore.getC1();
}
​
//此方法即为一级分类下拉菜单的change事件(选中值的时候会触发,保证一级分类ID有了)
const handler = () => {
    //需要将二级、三级分类的数据清空
    categoryStore.c2Id = '';
    categoryStore.c3Arr = [];
    categoryStore.c3Id = '';
    //通知仓库获取二级分类的数据
    categoryStore.getC2();
}
//此方法即为二级分类下拉菜单的change事件(选中值的时候会触发,保证二级分类ID有了)
const handler1 = () => {
    //清理三级分类的数据
    categoryStore.c3Id = '';
    categoryStore.getC3();
}
​
//接受父组件传递过来scene
defineProps(['scene']);
​
</script><style scoped></style>
  1. 父组件 project\src\views\product\attr\index.vue 传值

    并且如果三级分类没有id (未被选中)则禁用掉下面的属性添加按钮

    <template>
      <div>
        <!-- 三级分类全局组件 +++++++++++ -->
        <Category :scene="scene" />
        <el-card style="margin: 10px 0px">
          <el-button type="primary" size="default" icon="Plus"  :disabled="categoryStore.c3Id ? false : true">添加属性</el-button>
          <el-table border style="margin: 10px 0px">
            <el-table-column label="序号" type="index" align="center" width="80px" ></el-table-column>
            <el-table-column label="属性名称" width="120px" ></el-table-column>
            <el-table-column label="属性值名称"></el-table-column>
            <el-table-column label="操作" width="120px"></el-table-column>
          </el-table>
        </el-card>
      </div>
    </template>
    <script setup lang="ts">
    import { ref, reactive } from 'vue'//定义card组件内容切换变量 ++++++++++++
    let scene = ref<number>(0);//scene=0,显示table,scene=1,展示添加与修改属性结构</script>
    <style lang="scss" scoped></style>
2.3 已有属性与属性值展示业务(属性列表显示)

功能实现分析: 实现属性列表的显示,实现方案是监听三级分类,当三级分类标签发生变化时,且值不为空时,根据三级分类的id获取列表数据,当一级标签发生变化时需要清空之前的数据。

<template>
  <div>
    <!-- 三级分类全局组件 :scene="scene" 当下面的列表变成属性编辑的时候需要将三级分类的组件禁用掉,只能显示 -->
    <Category :scene="scene" />
    <el-card style="margin: 10px 0px">
      <div v-show="scene == 0">
                <el-button @click="addAttr" type="primary" size="default" icon="Plus"
                    :disabled="categoryStore.c3Id ? false : true">添加属性</el-button>
                <el-table border style="margin:10px 0px" :data="attrArr">
                    <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                    <el-table-column label="属性名称" width="120px" prop="attrName"></el-table-column>
                    <el-table-column label="属性值名称">
                        <template #="{ row, $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>
    </el-card>
  </div>
</template>
<script setup lang="ts">
//组合式API函数watch
import { watch, ref, reactive, nextTick, onBeforeUnmount } from 'vue';
//引入获取已有属性与属性值接口
import { reqAttr, reqAddOrUpdateAttr, reqRemoveAttr } from '@/api/product/attr';
import type { AttrResponseData, Attr, AttrValue } from '@/api/product/attr/type';
//获取分类的仓库
import useCategoryStore from '@/store/modules/category';
import { ElMessage } from 'element-plus';
let categoryStore = useCategoryStore();
//存储已有的属性与属性值
let attrArr = ref<Attr[]>([]);
​
//定义card组件内容切换变量
let scene = ref<number>(0);//scene=0,显示table,scene=1,展示添加与修改属性结构//监听仓库三级分类ID变化
watch(() => categoryStore.c3Id, () => {
    //清空上一次查询的属性与属性值
    attrArr.value = [];
    //保证三级分类得有才能发请求
    if (!categoryStore.c3Id) return;
    //获取分类的ID
    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 = () => {
​
      //切换为添加与修改属性的结构
    scene.value = 1;
}
​
//table表格修改已有属性按钮的回调
const updateAttr = (row: Attr) => {
    //切换为添加与修改属性的结构
    scene.value = 1;
​
}
​
//取消按钮的回调
const cancel = () => {
    scene.value = 0;
}
​
//删除某一个已有的属性方法回调
const deleteAttr = async (attrId: number) => {
​
​
​
}
​
</script>
<style lang="scss" scoped></style>
2.4 属性列表添加与修改属性以及删除实现

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" type="primary" size="default" icon="Plus"
                    :disabled="categoryStore.c3Id ? false : true">添加属性</el-button>
                <el-table border style="margin:10px 0px" :data="attrArr">
                    <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                    <el-table-column label="属性名称" width="120px" prop="attrName"></el-table-column>
                    <el-table-column label="属性值名称">
                        <template #="{ row, $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">
//组合式API函数watch
import { watch, ref, reactive, nextTick, onBeforeUnmount } from 'vue';
//引入获取已有属性与属性值接口
import { reqAttr, reqAddOrUpdateAttr, reqRemoveAttr } from '@/api/product/attr';
import type { AttrResponseData, Attr, AttrValue } from '@/api/product/attr/type';
//获取分类的仓库
import useCategoryStore from '@/store/modules/category';
import { ElMessage } from 'element-plus';
let categoryStore = useCategoryStore();
//存储已有的属性与属性值
let attrArr = ref<Attr[]>([]);
​
//定义card组件内容切换变量
let scene = ref<number>(0);//scene=0,显示table,scene=1,展示添加与修改属性结构
​
​
//收集新增的属性的数据
let attrParams = reactive<Attr>({
    attrName: "",//新增的属性的名字
    attrValueList: [//新增的属性值数组
    ],
    categoryId: '',//三级分类的ID
    categoryLevel: 3,//代表的是三级分类
})
//准备一个数组:将来存储对应的组件实例el-input
let inputArr = ref<any>([]);
​
//监听仓库三级分类ID变化
watch(() => categoryStore.c3Id, () => {
    //清空上一次查询的属性与属性值
    attrArr.value = [];
    //保证三级分类得有才能发请求
    if (!categoryStore.c3Id) return;
    //获取分类的ID
    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进行对象的合并
    // 使用  JSON.parse(JSON.stringify(row)) 包装一下解决 浅拷贝的问题
    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();
    })
​
    console.log(" attrParams.attrValueList ===>", attrParams.attrValueList);
​
}
​
//保存按钮的回调
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 lang="scss" scoped></style>