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

一、真实接口替换mock接口

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

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

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

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

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

// 统一管理项目用户相关的接口
import request from '@/utils/request'
import type {
  loginForm,
  loginResponseData,
  userResponseData,
} from './type'
// 项目用户相关的请求地址
enum API {
  LOGIN_URL = '/admin/acl/index/login',
  USERINFO_URL = '/admin/acl/index/info',
  LOGOUT_URL = '/admin/acl/index/logout',
}

// 暴露请求函数
// 登录接口
export const reqLogin = (data: loginForm) =>
  request.post<any, loginResponseData>(API.LOGIN_URL, data)
// 获取用户信息
export const reqUserInfo = () =>
  request.get<any, userResponseData>(API.USERINFO_URL)
// 退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

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

// 创建用户相关的小仓库
import { defineStore } from 'pinia'
// 接口
import { reqLogin,reqUserInfo,reqLogout } from '@/api/user'
// 引入路由(常量路由)
import { constantRoute } from '@/router/routes';
// 引入数据类型
// import type { loginForm, loginResponseData } from '@/api/user/type'
import type { UserState } from './types/type'
// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'

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

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

二、接口ts类型定义

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

// 定义用户相关数据的ts类型
// 用户登录接口携带参数的ts类型
export interface loginFormData {
  username: string
  password: string
}

// 定义全部接口返回数据都拥有的ts类型
export interface ResponseData {
  code: number
  message: string
  ok: boolean
}

// 定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {
  data: string
}

// 定义获取用户信息返回的数据类型
export interface userInfoResponseData extends ResponseData {
  data: {
    routes: string[]
    buttons: string[]
    roles: string[]
    name: string
    avatar: string
  }
}

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

// 统一管理项目用户相关的接口
import request from '@/utils/request'
import type {
  loginFormData,
  loginResponseData,
  userInfoResponseData,
} from './type'
// 项目用户相关的请求地址
enum API {
  LOGIN_URL = '/admin/acl/index/login',
  USERINFO_URL = '/admin/acl/index/info',
  LOGOUT_URL = '/admin/acl/index/logout',
}

// 暴露请求函数
// 登录接口
export const reqLogin = (data: loginFormData) =>
  request.post<any, loginResponseData>(API.LOGIN_URL, data)
// 获取用户信息
export const reqUserInfo = () =>
  request.get<any, userInfoResponseData>(API.USERINFO_URL)
// 退出登录
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

三、品牌管理模块

1.静态模块搭建

Pagination 分页 | Element Plus

表单组件table

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

        table-column

        ---label:设置列的标题

        ---width:设置列的宽度

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

分页器pagination

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

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

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

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

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

src/views/product/trademark/index.vue

<template>
  <el-card>
    <!-- 卡片顶部添加品牌按钮 -->
    <el-button type="primary" size="default" icon="Plus">添加品牌</el-button>
    <!-- 表格组件:用于展示已有的平台数据 -->
    <!-- 
        table
        ---border:可以设置表格纵向是否有边框
        table-column
        ---label:设置列的标题
        ---width:设置列的宽度
        ---align:设置列的对齐方式(left、center、right)
     -->
    <el-table style="margin: 10px 0;" border>
      <el-table-column label="序号" width="80px" align="center"></el-table-column>
      <el-table-column label="品牌名称"></el-table-column>
      <el-table-column label="品牌LOGO"></el-table-column>
      <el-table-column label="品牌操作"></el-table-column>
    </el-table>
    <!-- 分页器组件
        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>
</template>
 
<script setup lang="ts">
// 引入组合式API函数ref
import {ref} from 'vue'
// 当前页码
let pageNo = ref<number>(1)
// 每一页展示多少条数据
let limit = ref<number>(3)
</script>
 
<style scoped></style>

 2.品牌管理模块数据展示

(1)书写trademark接口文件src/api/product/trademark/index.ts

// 书写品牌管理模块
import request from "@/utils/request";
// 引入数据类型
import type {TrademarkResponeData} from './type'
// 品牌管理模块接口地址
enum API {
    // 获取已有品牌接口
    TRADEMARK_URL = '/admin/product/baseTrademark/'
}
// 获取已有品牌的接口方法
// page:获取第几页 ---默认第一页
// limit:获取几个已有品牌的数据
export const reqHasTrademark = (page: number, limit: number) =>
  request.get<any, TrademarkResponeData>(API.TRADEMARK_URL + `${page}/${limit}`)

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

export interface ResponseData {
  code: number
  message: string
  ok: boolean
}

// 已有的品牌的ts数据类型
export interface Trademark {
  id?: number
  tmName: string
  logoUrl: string
}

// 包含全部品牌数据的ts类型
export type Records = Trademark[]

// 获取的已有全部品牌的数据ts类型
export interface TrademarkResponeData extends ResponseData {
  data: {
    records: Records
    total: number
    size: number
    current: number
    orders: []
    optimizeCountSql: boolean
    hitCount: boolean
    countId: null
    maxLimit: null
    searchCount: boolean
    pages: number
  }
}

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

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

<template>
    <el-card>
        <!-- 卡片顶部添加品牌按钮 -->
        <el-button type="primary" size="default" icon="Plus">添加品牌</el-button>
        <!-- 表格组件:用于展示已有的平台数据 -->
        <!-- 
        table
        ---border:可以设置表格纵向是否有边框
        table-column
        ---label:设置列的标题
        ---width:设置列的宽度
        ---align:设置列的对齐方式(left、center、right)
     -->
        <el-table style="margin: 10px 0;" border :data="trademarkArr">
            <el-table-column label="序号" width="80px" align="center" type="index"></el-table-column>
            <el-table-column label="品牌名称" prop="tmName"></el-table-column>
            <!-- table-column默认展示数据用的是div -->
            <el-table-column label="品牌LOGO">
                <template #="{ row }">
                    <img :src="row.logoUrl" style="width: 100px;height: 100px;">
                </template>
            </el-table-column>
            <el-table-column label="品牌操作">
                <!-- <template #="{ row,$index }"> -->
                <el-button type="primary" size="small" icon="Edit"></el-button>
                <el-button type="primary" size="small" icon="Delete"></el-button>
                <!-- </template> -->
            </el-table-column>
        </el-table>
        <!-- 分页器组件
        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>
</template>
 
<script setup lang="ts">
// 引入组合式API函数ref
import { ref,onMounted } from 'vue'
import { reqHasTrademark } from '@/api/product/trademark'
// 引入数据类型
import type {Records, TrademarkResponeData } from '@/api/product/trademark/type'
// 当前页码
let pageNo = ref<number>(1)
// 每一页展示多少条数据
let limit = ref<number>(3)
// 存储已有品牌数据总数
let total = ref<number>(0)
// 存储已有品牌的数据
let trademarkArr = ref<Records>([])
// 获取已有品牌的接口封装为一个函数:在任何情况下想获取数据,调用函数即可
const getHasTrademark = async () => {
    let result: TrademarkResponeData = await reqHasTrademark(pageNo.value, limit.value)
    if (result.code === 200) {
        // 存储已有品牌总个数
        total.value = result.data.total
        trademarkArr.value = result.data.records
    }
}
// 组件挂载完毕的钩子---发一次请求,获取第一页,一页三个已有品牌数据
onMounted(() => {
    getHasTrademark()
})
</script>
 
<style scoped></style>

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

Pagination 分页 | Element Plus

给pagination组件标签添加current-change、size-change事件,书写对应方法,在当前页码和下拉菜单(每页展示的数据条数)发生变化时触发getHasTrademark回调,重新获取数据进行展示。

<el-pagination @size-change="sizeChange" @current-change="getHasTrademark" v-model:current-page="pageNo"
      v-model:page-size="limit" :page-sizes="[3, 5, 7, 9]" :background="true"
      layout="prev, pager, next, jumper,->,sizes,total" :total="total" />
 
 
 const getHasTrademark = async (pager=1) => {
    // 当前页码
    pageNo.value=pager
.....
}
 
// 分页器当前页码发生变化的时候触发
// 对于当前页码发生变化自定义事件,组件pagination父组件回传了数据(当前的页码)
// const changePageNo = () =>{
//   // 当前页码发生变化的时候再次发送请求获取对应已有品牌数据展示
//   getHasTrademark()
// }
 
// 当下拉菜单发生变化的时候触发此方法
// 这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
  // 当前每一页的数据发生变化的时候,当前页码归1
  getHasTrademark()
}

 4.对话框dialog

4.1静态对话框dialog

Upload 上传 | Element Plus

src/views/product/trademark/index.vue 

<!-- 对话框组件:在添加或修改品牌时的结构 -->
    <el-dialog v-model="dialogFormVisible" title="修改">
        <el-form style="width: 80%;">
            <el-form-item label="品牌名称" label-width="80px">
                <el-input placeholder="请你输入品牌名称"></el-input>
            </el-form-item>
            <el-form-item label="品牌logo" label-width="80px">
                <el-upload class="avatar-uploader" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
                    :show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
                    <img v-if="imageUrl" :src="imageUrl" class="avatar" />
                    <el-icon v-else class="avatar-uploader-icon">
                        <Plus />
                    </el-icon>
                </el-upload>
            </el-form-item>
        </el-form>
        <!-- 具名插槽 -->
        <template #footer>
            <el-button type="primary" size="default" @click="cancel">取消</el-button>
            <el-button type="primary" size="default" @click="confirm">确定</el-button>
        </template>
    </el-dialog>
...
// 控制对话框显示与隐藏
let dialogFormVisible = ref<boolean>(false)
....
// 添加按钮回调
const addTrademark = () => {
    // 对话框提示
    dialogFormVisible.value = true
}
// 修改按钮回调
const updateTrademark = () => {
    // 对话框提示
    dialogFormVisible.value = true
}
// 取消按钮
const cancel = () => {
    dialogFormVisible.value = false
}
// 确定按钮
const confirm = () => {
    dialogFormVisible.value = false
}

<style scoped>
.avatar-uploader .avatar {
    width: 178px;
    height: 178px;
    display: block;
}
</style>

<style>
.avatar-uploader .el-upload {
    border: 1px dashed var(--el-border-color);
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
    transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
    border-color: var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;
    text-align: center;
}
</style>

4.2对话框接口业务实现

(1)定义新增|修改接口

 src/api/product/trademark/index.ts

......
enum API {
  ......
    // 添加品牌
    ADDTRADEMARK_URL = '/admin/product/baseTrademark/save',
    // 修改已有品牌
    UPDATETRADEMARK_URL = '/admin/product/baseTrademark/update'
}
 
// 添加与修改已有品牌接口方法
export const reqAddOrUpdateTrademark = (data: Trademark) => {
    // 修改已有品牌的数据
    if (data.id) {
        return request.put<any, any>(API.UPDATETRADEMARK_URL, data)
    } else {
        // 新增品牌
        return request.post<any, any>(API.ADDTRADEMARK_URL, data)
    }
}

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

src/views/product/trademark/index.vue  

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

<template>
    <el-card>
        <!-- 卡片顶部添加品牌按钮 -->
        <el-button type="primary" size="default" icon="Plus" @click="addTrademark">添加品牌</el-button>
        <!-- 表格组件:用于展示已有的平台数据 -->
        <!-- 
        table
        ---border:可以设置表格纵向是否有边框
        table-column
        ---label:设置列的标题
        ---width:设置列的宽度
        ---align:设置列的对齐方式(left、center、right)
     -->
        <el-table style="margin: 10px 0;" border :data="trademarkArr">
            <el-table-column label="序号" width="80px" align="center" type="index"></el-table-column>
            <el-table-column label="品牌名称" prop="tmName"></el-table-column>
            <!-- table-column默认展示数据用的是div -->
            <el-table-column label="品牌LOGO">
                <template #="{ row }">
                    <img :src="row.logoUrl" style="width: 100px;height: 100px;">
                </template>
            </el-table-column>
            <el-table-column label="品牌操作">
                <template #="{ row }">
                <el-button type="primary" size="small" icon="Edit" @click="updateTrademark(row)"></el-button>
                <el-button type="primary" size="small" icon="Delete"></el-button>
                </template>
            </el-table-column>
        </el-table>
        <!-- 分页器组件
        pagination
        v-model:current-page:设置分页器当前页码
        v-model:page-size:设置每一个展示数据条数
        page-sizes:用于设置下拉菜单数据
        background:设置分页器按钮的背景颜色
        layout:可以设置分页器六个子组件布局调整
     -->
        <el-pagination @current-change="getHasTrademark" @size-change="sizeChange" :pager-count="9"
            v-model:current-page="pageNo" v-model:page-size="limit" :page-sizes="[3, 5, 7, 9]" :background="true"
            layout="prev, pager, next, jumper,->,sizes,total" :total="total" />
    </el-card>
    <!-- 对话框组件:在添加或修改品牌时的结构 -->
    <el-dialog v-model="dialogFormVisible" :title="trademarkParams.id ? '修改品牌' : '添加品牌'">
        <el-form style="width: 80%;" :model="trademarkParams" :rules="rules" ref="formRef">
            <el-form-item label="品牌名称" label-width="100px" prop="tmName">
                <el-input placeholder="请你输入品牌名称" v-model="trademarkParams.tmName"></el-input>
            </el-form-item>
            <el-form-item label="品牌logo" label-width="100px" prop="logoUrl">
                <el-upload 
                    class="avatar-uploader" 
                    action="/api/admin/product/fileUpload"
                    :show-file-list="false" 
                    :on-success="handleAvatarSuccess" 
                    :before-upload="beforeAvatarUpload">
                    <img v-if="trademarkParams.logoUrl" :src="trademarkParams.logoUrl" class="avatar" />
                    <el-icon v-else class="avatar-uploader-icon">
                        <Plus />
                    </el-icon>
                </el-upload>
            </el-form-item>
        </el-form>
        <!-- 具名插槽 -->
        <template #footer>
            <el-button type="primary" size="default" @click="cancel">取消</el-button>
            <el-button type="primary" size="default" @click="confirm">确定</el-button>
        </template>
    </el-dialog>
</template>
 
<script setup lang="ts">
import { ElMessage, UploadProps } from 'element-plus'
// 引入组合式API函数ref
import { ref, onMounted,reactive,nextTick } from 'vue'
import { reqHasTrademark,reqAddOrUpdateTrademark } from '@/api/product/trademark'
// 引入数据类型
import type { Records, TrademarkResponeData,Trademark } from '@/api/product/trademark/type'
// 当前页码
let pageNo = ref<number>(1)
// 每一页展示多少条数据
let limit = ref<number>(3)
// 存储已有品牌数据总数
let total = ref<number>(0)
// 存储已有品牌的数据
let trademarkArr = ref<Records>([])
// 控制对话框显示与隐藏
let dialogFormVisible = ref<boolean>(false)
// 定义收集新增品牌数据
let trademarkParams = reactive<Trademark>({
    tmName: '',
    logoUrl: ''
})
let formRef = ref()
// 获取已有品牌的接口封装为一个函数:在任何情况下想获取数据,调用函数即可
const getHasTrademark = async (pager = 1) => {
    // 当前页码
    pageNo.value = pager
    let result: TrademarkResponeData = await reqHasTrademark(pageNo.value, limit.value)
    if (result.code === 200) {
        // 存储已有品牌总个数
        total.value = result.data.total
        trademarkArr.value = result.data.records
    }
}
// 组件挂载完毕的钩子---发一次请求,获取第一页,一页三个已有品牌数据
onMounted(() => {
    getHasTrademark()
});
// 分页器当前页码发生变化的时候触发
// 对于当前页码发生变化自定义事件,组件pagination父组件回传了数据(当前的页码)
// const changePageNo = () =>{
//   // 当前页码发生变化的时候再次发送请求获取对应已有品牌数据展示
//   getHasTrademark()
// }

// 当下拉菜单发生变化的时候触发此方法
// 这个自定义事件,分页器组件会将下拉菜单选中数据返回
const sizeChange = () => {
    // 当前每一页的数据发生变化的时候,当前页码归1
    // pageNo.value=1
    getHasTrademark()
}
// 添加按钮回调
const addTrademark = () => {
    // 对话框提示
    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是当前已有的品牌
const updateTrademark = (row:Trademark) => {
    // 清空校验规则错误提示信息
    nextTick(() => {
        formRef.value.clearValidate('tmName')
        formRef.value.clearValidate('logoUrl')
    })
    // 对话框提示
    dialogFormVisible.value = true
    // es6合并语法
    Object.assign(trademarkParams, row)
    // 修改
    // trademarkParams.id=row.id
    // trademarkParams.tmName=row.tmName
    // trademarkParams.logoUrl=row.logoUrl
}
// 取消按钮
const cancel = () => {
    dialogFormVisible.value = false
}
// 确定按钮
const confirm = async() => {
    // 在你发请求之前,要对于整个表单进行校验
    // 调用这个方法进行全部表单校验,如果校验全部通过,再执行后面的语句
    await formRef.value.validate()
    let result:any = await reqAddOrUpdateTrademark(trademarkParams)
    // 添加、修改
    if (result.code === 200) {
        // 关闭对话框
        dialogFormVisible.value = false;
        // 弹出提示信息
        ElMessage({
            type: 'success',
            message: trademarkParams.id ? '修改品牌成功' : '添加品牌成功'
        })
        // 再次发请求获取已有全部的品牌数据,
        getHasTrademark(trademarkParams.id ? pageNo.value : 1)
    } else {
        // 添加品牌失败
        ElMessage({
            type: 'error',
            message: trademarkParams.id ? '修改品牌失败' : '添加品牌失败'
        })
        // 关闭对话框
        dialogFormVisible.value = false;
    }
}
// 上传图片组件之前触发的钩子函数
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
    // 钩子是在上传成功之前触发,上传文件之前可以约束文件类型与大小
    // 要求:上传文件格式png|jpg|gif 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.error({
            type: 'error',
            message: '上传文件格式务必PNG|JPG|GIF'
        })
        return false
    }
}
// 图片上传成功钩子
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
    console.log(uploadFile);
    // response:即当前这次上传图片post请求服务器返回的数据
    // 收集上传图片的地址,添加一个新的品牌的时候带给服务器
    trademarkParams.logoUrl = response.data
    // 图片上传成功,清除掉对应图片校验错误提示信息
    formRef.value.clearValidate('logoUrl')
}
// 品牌名称自定义校验规则方法
const validatorTmName = (rule: any, value: any, callBack: any) => {
    // 是表单元素触发blur时候,会触发此方法
    console.log(rule);
    // 自定义校验规则
    if (value.trim().length >= 2) {
        callBack()
    } else {
        // 校验未通过返回的错误提示信息
        callBack(new Error('品牌名称位数大于等于两位'))
    }
}

// 品牌LOGO图片的自定义校验规则方法
const validatorLogoUrl = (rule: any, value: any, callBack: any) => {
    console.log(rule);
    // 如果图片上传
    if (value) {
        callBack()
    } else {
        callBack(new Error('LOGO图片务必上传'))
    }
}
// 表单校验规则
const rules = {
    // required:这个字段务必校验,表单前面出来五角星
    // trigger:代表触发校验规则时机(blur、change)
    tmName: [
        { required: true, trigger: 'blur', validator: validatorTmName }
    ],
    logoUrl: [
        { required: true, validator: validatorLogoUrl }
    ]
}
</script>

<style scoped>
.avatar-uploader .avatar {
    width: 178px;
    height: 178px;
    display: block;
}
</style>

<style>
.avatar-uploader .el-upload {
    border: 1px dashed var(--el-border-color);
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
    transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
    border-color: var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;
    text-align: center;
}
</style>

5. 品牌管理删除业务

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

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

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

(2)src/views/product/trademark/index.vue  

<el-popconfirm :title="`您确定要删除${row.tmName}吗?`" width="250px" icon="Delete" @confirm='removeTradeMark(row.id)'>
    <template #reference>
        <el-button type="danger" size="small" icon="Delete"></el-button>
    </template>
</el-popconfirm>
 
 
 
import { reqHasTrademark, reqAddOrUpdateTrademark, reqDeleteTrademark } from '@/api/product/trademark'
 
// 气泡确认框确定按钮的回调
const removeTradeMark = async (id: number) => {
  // 点击确认按钮删除已有品牌请求
  let result = await reqDeleteTrademark(id)
  if (result.code === 200) {
    // 删除成功提示信息
    ElMessage({
      type: 'success',
      message: '删除品牌成功'
    })
    // 再次获取已有的品牌数据
    getHasTrademark(trademarkArr.value.length > 1 ? pageNo.value : pageNo.value - 1)
  } else {
    ElMessage({
      type: 'error',
      message: '删除品牌失败'
    })
  }
}

四、属性管理模块

1.静态搭建

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

(1)分类全局组件

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

<template>
    <el-card>
        <el-form :inline="true">
            <el-form-item label="一级分类">
                <el-select style="width:150px;">
                    <el-option label="北京"></el-option>
                    <el-option label="上海"></el-option>
                    <el-option label="广州"></el-option>
                    <el-option label="深圳"></el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="二级分类">
                <el-select style="width:150px;">
                    <el-option label="北京"></el-option>
                    <el-option label="上海"></el-option>
                    <el-option label="广州"></el-option>
                    <el-option label="深圳"></el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="三级分类">
                <el-select style="width:150px;">
                    <el-option label="北京"></el-option>
                    <el-option label="上海"></el-option>
                    <el-option label="广州"></el-option>
                    <el-option label="深圳"></el-option>
                </el-select>
            </el-form-item>
        </el-form>
    </el-card>
</template>
 
<script setup lang="ts"></script>
 
<style scoped></style>

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

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

(3)属性管理组件,引入三级分类全局组件+使用el-table展示属性相关数据,实现属性管理组件的静态搭建:rc/views/product/attr/index.vue

<template>
  <!-- 三级分类全局组件 -->
  <Category />
  <el-card style="margin: 10px 0;">
    <el-button type="primary" size="default" icon="Plus">添加属性</el-button>
    <el-table border style="margin: 10px 0;">
      <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
      <el-table-column label="属性名称" width="120px"></el-table-column>
      <el-table-column label="属性值名称"></el-table-column>
      <el-table-column label="操作" width="120px"></el-table-column>
    </el-table>
 
  </el-card>
</template>
 
<script setup lang="ts"></script>
 
<style scoped></style>

2.分类全局组件(Category)

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

2.1接口定义

src/api/product/attr/index.ts

// 这里书写属性相关的API文件
import request from '@/utils/request'
import type { CategoryResponseData } from './type'
 
// 属性管理模块接口地址
enum API {
    // 获取一级分类接口地址
    C1_URL = '/admin/product/getCategory1',
    // 获取二级分类接口地址
    C2_URL = '/admin/product/getCategory2/',
    // 获取三级分类接口地址
    C3_URL = '/admin/product/getCategory3/',
}
 
// 获取一级分类的接口方法
export const reqC1 = () => request.get<any, CategoryResponseData>(API.C1_URL)
// 获取二级分类的接口方法
export const reqC2 = (category1: number | string) => request.get<any, CategoryResponseData>(API.C2_URL + category1)
// 获取三级分类的接口方法
export const reqC3 = (category2: number | string) => request.get<any, CategoryResponseData>(API.C3_URL + category2)

2.2 数据ts类型定义

src/api/product/attr/type.ts

// 分类相关的数据ts类型
export interface ResponseData {
    code: 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[]
}

2.3创建小仓库

 src/store/modules/attr/category.ts

// 商品分类全局组件的小仓库
import { defineStore } from 'pinia'
import { reqC1, reqC2, reqC3 } from '@/api/product/attr'
import type { CategoryResponseData } from '@/api/product/attr/type'
import type { CategoryState } from './types/type'
let useCategoryStore = defineStore('Category', {
    state: (): CategoryState => {
        return {
            // 存储一级分类的数据
            c1Arr: [],
            // 存储一级分类的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

2.4state数据ts类型定义

src/store/modules/types/type.ts 

import type { CategoryObj } from '@/api/product/attr/type'
 
// 定义分类仓库state对象的ts类型
export interface CategoryState {
  c1Id: string | number
  c1Arr: CategoryObj[]
  c2Arr: CategoryObj[]
  c2Id: string | number
  c3Arr: CategoryObj[]
  c3Id: string | number
}

2.5业务实现

src/components/Category/index.vue

<template>
    <el-card>
        <el-form :inline="true">
            <el-form-item label="一级分类">
                <el-select  v-model="categoryStore.c1Id" @change="handler" style="width:150px;">
                    <!-- label:即为展示数据 value:即为select下拉菜单收集的数据 -->
                    <el-option v-for="(c1,) in categoryStore.c1Arr" :key="c1.id" :label="c1.name"
                            :value="c1.id"></el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="二级分类">
                <el-select v-model="categoryStore.c2Id" @change="handler1" style="width:150px;">
                    <el-option v-for="(c2,) in categoryStore.c2Arr" :key="c2.id" :label="c2.name"
                        :value="c2.id"></el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="三级分类">
                <el-select v-model="categoryStore.c3Id" style="width:150px;">
                    <el-option v-for="(c3,) in categoryStore.c3Arr" :key="c3.id" :label="c3.name"
                            :value="c3.id"></el-option>
                </el-select>
            </el-form-item>
        </el-form>
    </el-card>
</template>
 
<script setup lang="ts">
// 引入生命周期函数钩子
import  { onMounted} from 'vue'
// 引入分类相关的仓库
import useCategoryStore from '@/store/modules/category'
let categoryStore = useCategoryStore()
// 组件挂在完毕,通知仓库发请求,获取一级分类数据
onMounted(()=>{
    // 获取一级分类数据
    getC1();
});
// 通知仓库获取一级分类的方法
const getC1 = () => {
    // 通知分类仓库发请求获取一级分类的数据
    categoryStore.getC1()
}
// 此方法即为一级分类下拉菜单的change事件(选中值的时候会触发,保证一级分类ID有了)
const handler = () => {
    // 需要将二级、三级分类的数据清空
    categoryStore.c2Id = ''
    categoryStore.c3Arr = []
    categoryStore.c3Id = ''
    // 通知仓库获取二级分类的数据
    categoryStore.getC2()
}
// 此方法即为二级分类下拉菜单的change事件(选中值的时候会触发,保证二级分类ID有了)
const handler1 = () => {
    // 清理三级分类的数据
    categoryStore.c3Id = ''
    categoryStore.getC3()
}
</script>
 
<style scoped></style>

src/views/product/attr/index.vue 

<template>
    <div>
            ....

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

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

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

(1)接口定义 

src/api/product/attr/index.ts

import type { ......, AttrResponseData, Attr  } from './type'
 
enum API {
    ......
    // 获取分类下已有属性与属性值
    ATTR_URL = '/admin/product/attrInfoList/',
    // 添加或修改已有的属性的接口
    ADDORUPDATEATTR_URL = '/admin/product/saveAttrInfo',
    //删除某一个已有的属性
    DELETEATTR_URL = '/admin/product/deleteAttr/',
}
 
// 获取对应分类下已有的属性与属性值接口
export const reqAttr = (category1Id: string | number, category2Id: string | number, category3Id: string | number) => request.get<any, AttrResponeData>(API.ATTR_URL + `${category1Id}/${category2Id}/${category3Id}`)
 
// 新增或修改已有属性的接口
export const reqAddOrUpdateAttr = (data: Attr) => request.post<any, any>(API.ADDORUPDATEATTR_URL, data)
 
//删除某一个已有的属性业务
export const reqRemoveAttr = (attrId: number) => request.delete<any, any>(API.DELETEATTR_URL + attrId)

 (2)数据ts类型定义

src/api/product/attr/type.ts

// 属性与属性值的ts类型
 
// 属性值对象的ts类型
export interface AttrValue {
    id?: number
    valueName: string
    attrId?: number
    flag: boolean
}
 
// 存储每一个属性值的数组类型
export type AttrValueList = AttrValue[]
 
// 属性对象的ts类型
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: AttrList
}

(3)业务实现

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

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

src/views/product/attr/index.vue

<template>
    <!-- 三级分类全局组件 -->
    <Category :scene="scene" />
    <el-card style="margin: 10px 0;">
        <div v-show="scene === 0">
            <!-- 通过是否有三级分类ID来判断按钮是否禁用,没有ID:true,有:false -->
            <el-button @click="addAttr" type="primary" size="default" icon="Plus"
                :disabled="categoryStore.c3Id ? false : true">添加属性</el-button>
            <el-table border style="margin: 10px 0;" :data="attrArr">
                <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                <el-table-column label="属性名称" width="120px" prop="attrName"></el-table-column>
                <el-table-column label="属性值名称">
                    <template #="{ row }">
                        <el-tag style="margin: 5px;" v-for="(item,) in row.attrValueList" :key="item.id">{{
                            item.valueName
                        }}</el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="120px">
                    <!-- row:已有属性对象 -->
                    <template #="{ row }">
                        <el-button type="warning" size="small" icon="Edit" @click="updateAttr(row)"></el-button>
                        <el-popconfirm :title="`你确定删除${row.attrName}?`" width="200px" @confirm="deleteAttr(row.id)">
                            <template #reference>
                                <el-button type="danger" size="small" icon="Delete"></el-button>
                            </template>
                        </el-popconfirm>
                    </template>
                </el-table-column>
            </el-table>
        </div>
        <div v-show="scene === 1">
            <!-- 展示添加属性与修改数据的结构 -->
            <el-form :inline="true">
                <el-form-item label="属性名称">
                    <el-input placeholder="请您输入属性名称" v-model="attrParams.attrName"></el-input>
                </el-form-item>
            </el-form>
            <el-button :disabled="attrParams.attrName ? false : true" type="primary" size="default" icon="Plus"
                @click="addAttrValue">添加属性值</el-button>
            <el-button size="default" @click="cancel">取消</el-button>
            <el-table border style="margin: 10px 0;" :data="attrParams.attrValueList">
                <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                <el-table-column label="属性值名称">
                    <!-- row:即为当前属性值对象 -->
                    <template #="{ row, $index }">
                        <el-input :ref="(vc: any) => inputArr[$index] = vc" v-if="row.flag" size="small"
                            @blur="toLook(row, $index)" placeholder="请您输入属性值名称" v-model="row.valueName"></el-input>
                        <div v-else @click="toEdit(row, $index)">{{ row.valueName }}</div>
                    </template>
                </el-table-column>
                <el-table-column label="属性值操作">
                    <template #="{ $index }">
                        <el-button type="danger" size="small" icon="Delete"
                            @click="attrParams.attrValueList.splice($index, 1)"></el-button>

                    </template>
                </el-table-column>
            </el-table>
            <el-button type="primary" size="default" @click="save"
                :disabled="attrParams.attrValueList.length > 0 ? false : true">保存</el-button>
            <el-button size="default" @click="cancel">取消</el-button>
        </div>
    </el-card>
</template>
 
<script setup lang="ts">
// 组合式API函数
import { nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue'
// 引入获取已有属性与属性值接口
import { reqAttr, reqAddOrUpdateAttr, reqRemoveAttr } from '@/api/product/attr'
import type { AttrResponseData, Attr, AttrValue } from '@/api/product/attr/type'
// 获取分类的仓库
import useCategoryStore from '@/store/modules/category'
import { ElMessage } from 'element-plus';
let categoryStore = useCategoryStore()
// 存储已有的属性与属性值
let attrArr = ref<Attr[]>([])
//定义card组件内容切换变量
let scene = ref<number>(0);//scene=0,显示table,scene=1,展示添加与修改属性结构
// 收集新增的属性的数据
let attrParams = reactive<Attr>({
    attrName: '', //新增属性名称
    categoryId: '', // 三级分类的ID
    categoryLevel: 3, // 代表的是三级分类
    attrValueList: [] //新增的属性值数组
})
// 准备一个数组:将来存储对应的组件实例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: '',
        categoryId: categoryStore.c3Id,
        categoryLevel: 3,
        attrValueList: []
    })
    //切换为添加与修改属性的结构
    scene.value = 1
}
//table表格修改已有属性按钮的回调
const updateAttr = (row: Attr) => {
    //切换为添加与修改属性的结构
    scene.value = 1
    //将已有的属性对象赋值给attrParams对象即为
    //ES6->Object.assign进行对象的合并
    Object.assign(attrParams, JSON.parse(JSON.stringify(row)))
}
//取消按钮的回调
const cancel = () => {
    scene.value = 0
}

// 添加属性值按钮的回调
const addAttrValue = () => {
    // 点击添加属性值按钮的时候,向数组添加一个属性值对象
    attrParams.attrValueList.push({
        valueName: '',
        flag: true
    })
    //获取最后el-input组件聚焦
    nextTick(() => {
        inputArr.value[attrParams.attrValueList.length - 1].focus()
    })
}

// 保存按钮的回调
const save = async () => {
    // 发请求
    let result: any = await reqAddOrUpdateAttr(attrParams)
    // 添加属性|修改已有的属性已经成功
    if (result.code === 200) {
        // 切换场景
        scene.value = 0
        // 提示信息
        ElMessage({
            type: 'success',
            message: attrParams.id ? '修改成功' : '添加成功'
        })
        // 获取全部已有的属性与属性值
        getAttr()
    } else {
        ElMessage({
            type: 'error',
            message: attrParams.id ? '修改失败' : '添加失败'
        })
    }
}

// 属性值表单失去焦点事件回调
const toLook = (row: AttrValue, $index: number) => {
    // 非法情况判断1
    if (row.valueName.trim() === '') {
        // 删除对应属性值为空的元素
        attrParams.attrValueList.splice($index, 1)
        // 提示信息
        ElMessage({
            type: 'error',
            message: '属性值不能为空'
        })
        return
    }
    // 非法情况2
    let repeat = attrParams.attrValueList.find(item => {
        // 切记把当前失去焦点属性值对象从当前数组扣除判断
        if (item !== row) {
            return item.valueName === row.valueName
        }
    })

    if (repeat) {
        // 将重复的属性值从数组当中删除
        attrParams.attrValueList.splice($index, 1)
        // 提示信息
        ElMessage({
            type: 'error',
            message: '属性值不能重复'
        })
        return
    }
    // 相应的属性值对象flag:变为false,展示div
    row.flag = false
}

// 属性值div点击事件回调
const toEdit = (row: AttrValue, $index: number) => {
    // 相应的属性值对象flag:变为true,展示input
    row.flag = true
    // nextTick:响应式数据发生变化,获取更新的DOM(组件实例)
    nextTick(() => {
        inputArr.value[$index].focus()
    })
}

//删除某一个已有的属性方法回调
const deleteAttr = async (attrId: number) => {
    //发相应的删除已有的属性的请求
    let result: any = await reqRemoveAttr(attrId)
    //删除成功
    if (result.code === 200) {
        ElMessage({
            type: 'success',
            message: '删除成功'
        })
        //获取一次已有的属性与属性值
        getAttr()
    } else {
        ElMessage({
            type: 'error',
            message: '删除失败'
        })
    }
}

//路由组件销毁的时候,把仓库分类相关的数据清空
onBeforeUnmount(() => {
    //清空仓库的数据
    categoryStore.$reset()
})
</script>
 
<style scoped></style>

 src/components/category/index.vue

<template>
    <el-card>
        <el-form :inline="true">
            <el-form-item label="一级分类">
                <el-select :disabled="scene == 0 ? false : true" v-model="categoryStore.c1Id" @change="handler" style="width:150px;">
                    <!-- label:即为展示数据 value:即为select下拉菜单收集的数据 -->
                    <el-option v-for="(c1,) in categoryStore.c1Arr" :key="c1.id" :label="c1.name"
                            :value="c1.id"></el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="二级分类">
                <el-select :disabled="scene == 0 ? false : true" v-model="categoryStore.c2Id" @change="handler1" style="width:150px;">
                    <el-option v-for="(c2,) in categoryStore.c2Arr" :key="c2.id" :label="c2.name"
                        :value="c2.id"></el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="三级分类">
                <el-select :disabled="scene == 0 ? false : true" v-model="categoryStore.c3Id" style="width:150px;">
                    <el-option v-for="(c3,) in categoryStore.c3Arr" :key="c3.id" :label="c3.name"
                            :value="c3.id"></el-option>
                </el-select>
            </el-form-item>
        </el-form>
    </el-card>
</template>
 
<script setup lang="ts">
// 引入生命周期函数钩子
import  { onMounted} from 'vue'
// 引入分类相关的仓库
import useCategoryStore from '@/store/modules/category'
let categoryStore = useCategoryStore()
// 组件挂在完毕,通知仓库发请求,获取一级分类数据
onMounted(()=>{
    // 获取一级分类数据
    getC1();
});
// 通知仓库获取一级分类的方法
const getC1 = () => {
    // 通知分类仓库发请求获取一级分类的数据
    categoryStore.getC1()
}
// 此方法即为一级分类下拉菜单的change事件(选中值的时候会触发,保证一级分类ID有了)
const handler = () => {
    // 需要将二级、三级分类的数据清空
    categoryStore.c2Id = ''
    categoryStore.c3Arr = []
    categoryStore.c3Id = ''
    // 通知仓库获取二级分类的数据
    categoryStore.getC2()
}
// 此方法即为二级分类下拉菜单的change事件(选中值的时候会触发,保证二级分类ID有了)
const handler1 = () => {
    // 清理三级分类的数据
    categoryStore.c3Id = ''
    categoryStore.getC3()
}
//接受父组件传递过来scene
defineProps(['scene']);
</script>
 
<style scoped></style>

五、SPU管理模块

SPU/SKU介绍

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

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

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

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

1、SPU静态页面搭建

src/views/product/spu/index.vue 

<template>
    <div>
        <!-- 三级分类 -->
        <Category :scene="scene"></Category>
        <el-card style="margin:10px 0px">
            <el-button type="primary" size="default" icon="Plus">添加SPU</el-button>
            <el-table style="margin:10px 0px" border>
                <el-table-column label="序号" type="index" align="center" width="80px"></el-table-column>
                <el-table-column label="名称"></el-table-column>
                <el-table-column label="SPU描述"></el-table-column>
                <el-table-column label="SPU操作"></el-table-column>
            </el-table>
            <!-- 分页器 -->
            <el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[3, 5, 7, 9]"
                :background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="400" />
        </el-card>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
// 场景的数据 
let scene = ref<number>(0)
// 分页器默认页码
let pageNo = ref<number>(1);
//每一页展示几条数据
let pageSize = ref<number>(3);
</script>

<style scoped></style>

2、业务实现

SPU模块包括:

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

(1) 接口定义

// SPU管理模块的接口
import request from '@/utils/request'
import {
  SpuData,
  HasSpuResponseData,
  AllTrademark,
  SpuHasImg,
  SaleAttrResponseData,
  HasSaleAttrResponseData,
  SkuData,
  SkuInfoData,
} from './type'

enum API {
  // 获取已有的SPU的数据
  HASSPU_URL = '/admin/product/',
  // 获取全部品牌的数据
  ALLTRADEMARK_URL = '/admin/product/baseTrademark/getTrademarkList',
  // 获取某个SPU下的全部的售卖产品的图片数据
  IMAGE_URL = '/admin/product/spuImageList/',
  // 获取某一个SPU下全部的已有的销售属性接口地址
  SPUHASSALEATTR_URL = '/admin/product/spuSaleAttrList/',
  // 获取整个项目全部的销售属性[颜色、版本、尺码]
  ALLSALEATTR_URL = '/admin/product/baseSaleAttrList',
  // 追加一个新的SPU
  ADDSPU_URL = '/admin/product/saveSpuInfo',
  // 更新已有的SPU
  UPDATESPU_URL = '/admin/product/updateSpuInfo',
  //追加一个新增的SKU地址
  ADDSKU_URL = '/admin/product/saveSkuInfo',
  //查看某一个已有的SPU下全部售卖的商品
  SKUINFO_URL = '/admin/product/findBySpuId/',
  //删除已有的SPU
  REMOVESPU_URL = '/admin/product/deleteSpu/',
}

// 获取某一个三级分类下已有的SPU数据
export const reqHasSpu = (
  page: number,
  limit: number,
  category3Id: number | string,
) =>
  request.get<any, HasSpuResponseData>(
    API.HASSPU_URL + `${page}/${limit}?category3Id=${category3Id}`,
  )
// 获取全部的SPU的品牌的数据
export const reqAllTrademark = () =>
  request.get<any, AllTrademark>(API.ALLTRADEMARK_URL)
// 获取某一个已有的SPU下全部商品的图片地址
export const reqSpuImageList = (spuId: number) =>
  request.get<any, SpuHasImg>(API.IMAGE_URL + spuId)
// 获取某一个已有的SPU拥有多少个销售属性
export const reqSpuHasSaleAttr = (spuId: number) =>
  request.get<any, SaleAttrResponseData>(API.SPUHASSALEATTR_URL + spuId)
// 获取全部的销售属性
export const reqAllSaleAttr = () =>
  request.get<any, HasSaleAttrResponseData>(API.ALLSALEATTR_URL)
// 添加一个新的SPU
// 更新已有的SPU
// data:即为新增的SPU|已有的SPU
export const reqAddOrUpdateSpu = (data: SpuData) => {
  // 如果SPU对象拥有ID,更新已有的SPU
  if (data.id) {
    return request.post<any, any>(API.UPDATESPU_URL, data)
  } else {
    return request.post<any, any>(API.ADDSPU_URL, data)
  }
}
//添加SKU的请求方法
export const reqAddSku = (data: SkuData) =>
  request.post<any, any>(API.ADDSKU_URL, data)

//获取SKU数据
export const reqSkuList = (spuId: number | string) =>
  request.get<any, SkuInfoData>(API.SKUINFO_URL + spuId)

//删除已有的SPU
export const reqRemoveSpu = (spuId: number | string) =>
  request.delete<any, any>(API.REMOVESPU_URL + spuId)

(2)数据ts类型定义

src/api/product/spu/type.ts

// 服务器全部接口返回的数据类型
export interface ResponeData {
    code: number
    message: string
    ok: boolean
}
 
// SPU数据的ts类型
export interface SpuData {
    category3Id: string | number
    id?: number
    spuName: string
    tmId: number | string
    description: string
    spuImageList: null | SpuImg[]
    spuSaleAttrList: null | SaleAttr[]
}
 
// 数组:元素都是已有SPU数据类型
export type Records = SpuData[]
 
// 定义获取已有的SPU接口返回的数据ts类型
export interface HasSpuResponeData extends ResponeData {
    data: {
        records: Records
        total: number
        size: number
        current: number
        searchCount: boolean
        pages: number
    }
}
 
// 品牌数据的ts类型
export interface Trademark {
    id: number
    tmName: string
    logoUrl: string
}
 
// 品牌接口返回的数据ts类型
export interface AllTrademark extends ResponeData {
    data: Trademark[]
}
 
// 商品图片的ts类型
export interface SpuImg {
    id?: number
    imgName?: string
    imgUrl?: string
    createTime?: string
    updateTime?: string
    spuId?: number
    name?: string
    url?: string
}
// 已有的SPU照片墙数据的类型
export interface SpuHasImg extends ResponeData {
    data: SpuImg[]
}
 
// 已有的销售属性值对象ts类型
export interface SaleAttrValue {
    id?: number
    createTime?: null
    updateTime?: null
    spuId?: number
    baseSaleAttrId: number | string
    saleAttrValueName: string
    saleAttrName?: string
    isChecked?: null
}
 
// 存储已有的销售属性值数组类型
export type SpuSaleAttrValueList = SaleAttrValue[]
 
// 销售属性对象ts类型
export interface SaleAttr {
    id?: number
    createTime?: null
    updateTime?: null
    spuId?: number
    baseSaleAttrId: number | string
    saleAttrName: string
    spuSaleAttrValueList: SpuSaleAttrValueList
    flag?: boolean
    saleAttrValue?: string
}
 
// SPU已有的销售属性接口返回数据ts类型
export interface SaleAttrResponseData extends ResponeData {
    data: SaleAttr[]
}
 
// 已有的全部SPU的返回数据ts类型
export interface HasSaleAttr {
    id: number
    name: string
}
 
export interface HasSaleAttrResponseData extends ResponeData {
    data: HasSaleAttr[]
}
 
export interface Attr {
    attrId: number | string //平台属性的ID
    valueId: number | string //属性值的ID
}
 
export interface saleArr {
    saleAttrId: number | string //属性ID
    saleAttrValueId: number | string //属性值的ID
}
export interface SkuData {
    category3Id: string | number //三级分类的ID
    spuId: string | number //已有的SPU的ID
    tmId: string | number //SPU品牌的ID
    skuName: string //sku名字
    price: string | number //sku价格
    weight: string | number //sku重量
    skuDesc: string //sku的描述
    skuAttrValueList?: Attr[]
    skuSaleAttrValueList?: saleArr[]
    skuDefaultImg: string //sku图片地址
}
 
//获取SKU数据接口的ts类型
export interface SkuInfoData extends ResponeData {
    data: SkuData[]
}

SPU模块包括:

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

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

src/views/product/spu/index.vue 

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

<template>
    <div>
        <!-- 三级分类 -->
        <Category :scene="scene" />
        <el-card style="margin: 10px 0;">
            <!-- v-if|v-show:都可以实现显示与隐藏 -->
            <div v-show="scene === 0">
                <el-button type="primary" size="default" icon="Plus" :disabled="!categoryStore.c3Id"
                    @click="addSpu">添加SPU</el-button>
                <!-- 展示已有SPU数据 -->
                <el-table border style="margin: 10px 0;" :data="records">
                    <el-table-column label="序号" type="index" align="center" width="80px" />
                    <el-table-column label="SPU名称" prop="spuName"></el-table-column>
                    <el-table-column label="SPU描述" prop="description" show-overflow-tooltip></el-table-column>
                    <el-table-column label="SPU操作">
                        <!-- row:即为已有的SPU对象 -->
                        <template #="{ row, $index }">
                            <el-button type="primary" size="small" icon="Plus" title="添加SKU"
                                @click="addSku(row)"></el-button>
                            <el-button type="warning" size="small" icon="Edit" title="修改SPU"
                                @click="upadteSpu(row)"></el-button>
                            <el-button type="info" size="small" icon="View" title="查看SKU列表"
                                @click="findSku(row)"></el-button>
                            <el-popconfirm :title="`你确定删除${row.spuName}?`" width="200px" @confirm="deleteSpu(row)">
                                <template #reference>
                                    <el-button type="danger" size="small" icon="Delete" title="删除SPU"></el-button>
                                </template>
                            </el-popconfirm>
                        </template>
                    </el-table-column>
                </el-table>
                <!-- 分页器 -->
                <el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[3, 5, 7, 9]"
                    ::background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="total"
                    @current-change="getHasSpu" @size-change="handleSizeChange" />
            </div>
            <!-- 添加SPU|修改SPU子组件 -->
            <SpuForm ref="spu" v-show="scene === 1" @changeScene="changeScene"></SpuForm>
            <!-- 添加SKU子组件 -->
            <SkuForm ref="sku" v-show="scene === 2" @changeScene="changeScene"></SkuForm>
            <!-- dialog对话框:展示已有的SKU数据 -->
            <el-dialog title="SKU列表" v-model="show">
                <el-table :data="skuArr" border>
                    <el-table-column label="SKU名称" prop="skuName"></el-table-column>
                    <el-table-column label="SKU价格" prop="price"></el-table-column>
                    <el-table-column label="SKU重量" prop="weight"></el-table-column>
                    <el-table-column label="SKU图片">
                        <template #="{ row, $index }">
                            <img :src="row.skuDefaultImg" style="width: 100px;height: 100px;">
                        </template>
                    </el-table-column>
                </el-table>
            </el-dialog>
        </el-card>

    </div>
</template>
 
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue';
// 引入分类的仓库
import useCategoryStore from '@/store/modules/category';
import { HasSpuResponseData, Records, SkuData, SkuInfoData, SpuData } from '@/api/product/spu/type';
import { reqHasSpu, reqRemoveSpu, reqSkuList } from '@/api/product/spu';
import SpuForm from './spuForm.vue'
import SkuForm from './skuForm.vue'
import { ElMessage } from 'element-plus';
let categoryStore = useCategoryStore()
// 场景的数据
let scene = ref<number>(0) // 0:显示已有SPU  1:添加|修改已有SPU  2:添加SKU
// 分页器默认页码
let pageNo = ref<number>(1)
// 每一页展示几条数据
let pageSize = ref<number>(3)
// 存储已有SPU的数据
let records = ref<Records>([])
// 存储已有SPU总个数
let total = ref<number>(0)
// 获取子组件实例SpuForm
let spu = ref<any>()
// 获取子组件示例SkuForm
let sku = ref<any>()
//存储全部的SKU数据
let skuArr = ref<SkuData[]>([])
let show = ref<boolean>(false)

// 监听三级分类ID变化
watch(() => categoryStore.c3Id, () => {
    // 务必保证有三级分类的ID
    if (!categoryStore.c3Id) return
    getHasSpu()
})

// 此方法执行:可以获取某一个三级分类下全部的已有的SPU
const getHasSpu = async (pager = 1) => {
    // 修改当前页码
    pageNo.value = pager
    let result: HasSpuResponseData = await reqHasSpu(pageNo.value, pageSize.value, categoryStore.c3Id)
    if (result.code === 200) {
        records.value = result.data.records
        total.value = result.data.total
    }
}

// 分页器下拉菜单发生变化的时候触发
const handleSizeChange = () => {
    getHasSpu()
}

// 添加新的SPU按钮的回调
const addSpu = () => {
    // 切换为场景1:添加与修改已有SPU结构->SpuForm
    scene.value = 1
    //点击添加SPU按钮,调用子组件的方法初始化数据
    spu.value.initAddSpu(categoryStore.c3Id)
}

// 修改已有的SPU的按钮的回调
const upadteSpu = (row: SpuData) => {
    // 切换为场景1:添加与修改已有SPU结构->SpuForm
    scene.value = 1
    // 调用子组件实例方法获取完整已有的SPU数据
    spu.value.initHasSpuData(row)
}

// 子组件SpuForm绑定自定义事件:目前是让子组件通知父组件切换场景为0
const changeScene = (obj: any) => {
    // 子组件SpuForm点击取消变为场景0:展示已有的SPU
    scene.value = obj.flag
    if (obj.params === 'update') {
        //更新留在当前页
        getHasSpu(pageNo.value)
    } else {
        //添加留在第一页
        getHasSpu()
    }
}

// 添加sku按钮的回调
const addSku = (row: SpuData) => {
    //点击添加SKU按钮切换场景为2
    scene.value = 2;
    // 调用子组件的方法,初始化添加SKU的数据
    sku.value.initSkuData(categoryStore.c1Id, categoryStore.c2Id, row)
}

//查看SKU列表的数据
const findSku = async (row: SpuData) => {
    let result: SkuInfoData = await reqSkuList((row.id as number))
    if (result.code === 200) {
        skuArr.value = result.data
        //对话框显示出来
        show.value = true
    }
}

//删除已有的SPU按钮的回调
const deleteSpu = async (row: SpuData) => {
    let result: any = await reqRemoveSpu((row.id as number))
    if (result.code === 200) {
        ElMessage({
            type: 'success',
            message: '删除成功'
        })
        //获取剩余SPU数据
        getHasSpu(records.value.length > 1 ? pageNo.value : pageNo.value - 1)
    } else {
        ElMessage({
            type: 'error',
            message: '删除失败'
        })
    }
}

//路由组件销毁前,情况仓库关于分类的数据
onBeforeUnmount(() => {
    categoryStore.$reset()
})
</script>
 
<style scoped></style>

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

 src/views/product/spu/spuForm.vue

思路解析:

1.先搭建好静态页面。

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

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

3.获取已有的SPU数据。

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

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

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

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

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

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

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

 src/views/product/spu/spuForm.vue

<template>
    <el-form label-width="100px">
        <el-form-item label="SPU名称">
            <el-input placeholder="请你输入SPU名称" v-model="spuParams.spuName"></el-input>
        </el-form-item>
        <el-form-item label="SPU品牌">
            <el-select placeholder="请你选中品牌" v-model="spuParams.tmId">
                <el-option v-for="item in allTrademark" :key="item.id" :label="item.tmName" :value="item.id"></el-option>
            </el-select>
        </el-form-item>
        <el-form-item label="SPU描述">
            <el-input type="textarea" placeholder="请你输入描述" v-model="spuParams.description"></el-input>
        </el-form-item>
        <el-form-item label="SPU照片">
            <!-- v-model:fileList->展示默认图片 
                 action:上传图片的接口地址
                 list-type:文件列表的类型
            -->
            <el-upload v-model:file-list="imgList" action="api/admin/product/fileUpload" list-type="picture-card"
                :on-preview="handlePictureCardPreview" :on-remove="handleRemove" :before-upload="handlerUpload">
                <el-icon>
                    <Plus />
                </el-icon>
            </el-upload>
            <el-dialog v-model="dialogVisible">
                <img w-full :src="dialogImageUrl" alt="Preview Image" style="width: 100%;height: 100%;" />
            </el-dialog>
        </el-form-item>
        <el-form-item label="SPU销售属性">
            <!-- 展示销售属性的下拉菜单 -->
            <el-select v-model="saleAttrIdAndValueName"
                :placeholder="unSelectSaleAttr.length ? `还有未选择${unSelectSaleAttr.length}个` : '无'">
                <el-option :value="`${item.id}:${item.name}`" v-for="item in unSelectSaleAttr" :key="item.id"
                    :label="item.name"></el-option>
            </el-select>
            <el-button @click="addSaleAttr" :disabled="saleAttrIdAndValueName ? false : true" style="margin-left: 10px;"
                type="primary" size="default" icon="Plus">添加销售属性</el-button>
            <!-- table展示销售属性与属性值的地方 -->
            <el-table style="margin: 10px 0;" border :data="saleAttr">
                <el-table-column label="序号" type="index" align="center" width="80px" />
                <el-table-column label="属性名" width="100px" prop="saleAttrName"></el-table-column>
                <el-table-column label="属性值">
                    <template #="{ row }">
                        <el-tag @close="row.spuSaleAttrValueList.splice(index, 1)" style="margin:0px 5px"
                            v-for="(item, index) in row.spuSaleAttrValueList" :key="item.id" class="mx-1" closable>
                            {{ item.saleAttrValueName }}
                        </el-tag>
                        <el-input @blur="toLook(row)" v-model="row.saleAttrValue" v-if="row.flag === true"
                            placeholder="请你输入属性值" size="small" style="width: 100px;"></el-input>
                        <el-button v-else @click="toEdit(row)" type="success" size="small" icon="Plus"></el-button>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="100px">
                    <template #="{ row, $index }">
                        <el-button type="danger" size="small" icon="Delete" @click="saleAttr.splice($index, 1)"></el-button>
                    </template>
                </el-table-column>
            </el-table>
        </el-form-item>
        <el-form-item>
            <el-button :disabled="saleAttr.length > 0 ? false : true" type="primary" size="default"
                @click="save">保存</el-button>
            <el-button size="default" @click="cancel">取消</el-button>
        </el-form-item>
    </el-form>
</template>
 
<script setup lang="ts">
import { reqAddOrUpdateSpu, reqAllSaleAttr, reqAllTrademark, reqSpuHasSaleAttr, reqSpuImageList } from '@/api/product/spu';
import { AllTrademark, HasSaleAttr, HasSaleAttrResponseData, SaleAttr, SaleAttrResponseData, SaleAttrValue, SpuData, SpuHasImg, SpuImg, Trademark } from '@/api/product/spu/type';
import { ElMessage } from 'element-plus';
import { computed, ref } from 'vue';
let $emit = defineEmits(['changeScene'])
// 点击取消按钮:通知父组件切换场景为1,展示已有的SPU数据
const cancel = () => {
    $emit('changeScene', { flag: 0, params: 'update' })
}

// 存储已有的SPU这些数据
// 品牌
let allTrademark = ref<Trademark[]>([])
// 商品图片
let imgList = ref<SpuImg[]>([])
// 已有的SPU销售属性
let saleAttr = ref<SaleAttr[]>([])
// 全部销售属性
let allSaleAttr = ref<HasSaleAttr[]>([])
//控制对话框的显示与隐藏
let dialogVisible = ref<boolean>(false)
//存储预览图片地址
let dialogImageUrl = ref<string>('')
let spuParams = ref<SpuData>({
    category3Id: "",//收集三级分类的ID
    spuName: "",//SPU的名字
    description: "",//SPU的描述
    tmId: '',//品牌的ID
    spuImageList: [],
    spuSaleAttrList: [],
})
//将来收集还未选择的销售属性的ID与属性值的名字
let saleAttrIdAndValueName = ref<string>('')
// 子组件书写一个方法
const initHasSpuData = async (spu: SpuData) => {
    //存储已有的SPU对象,将来在模板中展示
    spuParams.value = spu
    // spu:即为父组件传递过来的已有的SPU对象【不完整】
    // 获取全部品牌的数据
    let result1: AllTrademark = await reqAllTrademark()
    // 获取某一个品牌旗下全部售卖商品的图片
    let result2: SpuHasImg = await reqSpuImageList((spu.id as number))
    // 获取已有的SPU销售属性的数据
    let result3: SaleAttrResponseData = await reqSpuHasSaleAttr((spu.id as number))
    // 获取整个项目全部SPU的销售属性
    let result4: HasSaleAttrResponseData = await reqAllSaleAttr()
    // 存储全部品牌的数据
    allTrademark.value = result1.data
    // SPU对应商品图片
    imgList.value = result2.data.map(item => {
        return {
            name: item.imgName,
            url: item.imgUrl
        }
    })
    // 存储已有的SPU的销售属性
    saleAttr.value = result3.data
    // 存储全部的销售属性
    allSaleAttr.value = result4.data
}
//照片墙点击预览按钮的时候触发的钩子
const handlePictureCardPreview = (file: any) => {
    dialogImageUrl.value = file.url
    //对话框弹出来
    dialogVisible.value = true
}
//照片墙删除文件钩子
const handleRemove = (file: any) => {
    console.log(file);
}
//照片墙上传成功之前的钩子约束文件的大小与类型
const handlerUpload = (file: any) => {
    if (file.type === 'image/png' || file.type === 'image/jpeg' || file.type === 'image/gif') {
        if (file.size / 1024 / 1024 < 3) {
            return true
        } else {
            ElMessage({
                type: 'error',
                message: '上传文件务必小于3M'
            })
            return false
        }
    } else {
        ElMessage({
            type: 'error',
            message: '上传文件务必PNG|JPG|GIF'
        })
        return false
    }
}

//计算出当前SPU还未拥有的销售属性
let unSelectSaleAttr = computed(() => {
    //全部销售属性:颜色、版本、尺码
    //已有的销售属性:颜色、版本
    let unSelectArr = allSaleAttr.value.filter(item => {
        return saleAttr.value.every(item1 => {
            return item.name !== item1.saleAttrName
        })
    })
    return unSelectArr;
})

//添加销售属性的方法
const addSaleAttr = () => {
    /*
    "baseSaleAttrId": number,
    "saleAttrName": string,
    "spuSaleAttrValueList": SpuSaleAttrValueList
    */
    const [baseSaleAttrId, saleAttrName] = saleAttrIdAndValueName.value.split(':')
    //准备一个新的销售属性对象:将来带给服务器即可
    let newSaleAttr: SaleAttr = {
        baseSaleAttrId,
        saleAttrName,
        spuSaleAttrValueList: []
    }
    //追加到数组当中
    saleAttr.value.push(newSaleAttr)
    //清空收集的数据
    saleAttrIdAndValueName.value = ''
}

//属性值按钮的点击事件
const toEdit = (row: SaleAttr) => {
    //点击按钮的时候,input组件不就不出来->编辑模式
    row.flag = true
    row.saleAttrValue = ''
}
//表单元素失却焦点的事件回调
const toLook = (row: SaleAttr) => {
    //整理收集的属性的ID与属性值的名字
    const { baseSaleAttrId, saleAttrValue } = row
    //整理成服务器需要的属性值形式
    let newSaleAttrValue: SaleAttrValue = {
        baseSaleAttrId,
        saleAttrValueName: (saleAttrValue as string)
    }
    //非法情况判断
    if ((saleAttrValue as string).trim() === '') {
        ElMessage({
            type: 'error',
            message: '属性值不能为空的'
        })
        return;
    }
    //判断属性值是否在数组当中存在
    let repeat = row.spuSaleAttrValueList.find(item => {
        return item.saleAttrValueName === saleAttrValue
    })
    if (repeat) {
        ElMessage({
            type: 'error',
            message: '属性值重复'
        })
        return;
    }
    //追加新的属性值对象
    row.spuSaleAttrValueList.push(newSaleAttrValue)
    //切换为查看模式
    row.flag = false
}

// 保存按钮的回调
const save = async () => {
    //整理参数
    //发请求:添加SPU|更新已有的SPU
    //成功
    //失败
    //1:照片墙的数据
    spuParams.value.spuImageList = imgList.value.map((item: any) => {
        return {
            imgName: item.name,//图片的名字
            imaUrl: (item.response && item.response.data) || item.url
        }
    })
    //2:整理销售属性的数据
    spuParams.value.spuSaleAttrList = saleAttr.value
    let result = await reqAddOrUpdateSpu(spuParams.value)
    if (result.code === 200) {
        ElMessage({
            type: 'success',
            message: spuParams.value.id ? '更新成功' : '添加成功'
        })
        $emit('changeScene', { flag: 0, params: spuParams.value.id ? 'update' : 'add' })
    } else {
        ElMessage({
            type: 'success',
            message: spuParams.value.id ? '更新失败' : '添加失败'
        })
    }
}

//添加一个新的SPU初始化请求方法
const initAddSpu = async (c3Id: number | string) => {
    // 清空数据
    Object.assign(spuParams.value, {
        category3Id: "",//收集三级分类的ID
        spuName: "",//SPU的名字
        description: "",//SPU的描述
        tmId: '',//品牌的ID
        spuImageList: [],
        spuSaleAttrList: [],
    })
    // 清空照片
    imgList.value = []
    // 清空销售属性
    saleAttr.value = []
    saleAttrIdAndValueName.value = ''
    spuParams.value.category3Id = c3Id
    //获取全部品牌的数据
    let result: AllTrademark = await reqAllTrademark()
    let result1: HasSaleAttrResponseData = await reqAllSaleAttr()
    // 存储数据
    allTrademark.value = result.data
    allSaleAttr.value = result1.data
}
// 对外暴露
defineExpose({ initHasSpuData, initAddSpu })

</script>
 
<style scoped></style>

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

1.skuform静态搭建

src/views/product/spu/skuForm.vue

<template>
    <el-form label-width="100px">
        <el-form-item label="sku名称">
            <el-input placeholder="请输入名称"></el-input>
        </el-form-item>
        <el-form-item label="sku价格">
            <el-input placeholder="sku价格" type="number"></el-input>
        </el-form-item>
        <el-form-item label="sku重量">
            <el-input placeholder="sku重量(克)" type="number"></el-input>
        </el-form-item>
        <el-form-item label="sku描述">
            <el-input placeholder="请输入描述" type="textarea"></el-input>
        </el-form-item>
        <el-form-item label="平台属性">
            <el-form :inline="true">
                <el-form-item label="手机一级">
                    <el-select style="width:200px;">
                        <el-option :label="123"></el-option>
                    </el-select>
                </el-form-item>
                <el-form-item label="手机一级">
                    <el-select style="width:200px;">
                        <el-option :label="123"></el-option>
                    </el-select>
                </el-form-item>
                <el-form-item label="手机一级">
                    <el-select style="width:200px;">
                        <el-option :label="123"></el-option>
                    </el-select>
                </el-form-item>
                
            </el-form>
        </el-form-item>
        <el-form-item label="销售属性">
            <el-form :inline="true">
                <el-form-item label="手机一级">
                    <el-select style="width:200px;">
                        <el-option :label="123"></el-option>
                    </el-select>
                </el-form-item>
                <el-form-item label="手机一级">
                    <el-select style="width:200px;">
                        <el-option :label="123"></el-option>
                    </el-select>
                </el-form-item>
                <el-form-item label="手机一级">
                    <el-select style="width:200px;">
                        <el-option :label="123"></el-option>
                    </el-select>
                </el-form-item>
            </el-form>
        </el-form-item>
        <el-form-item label="图片">
            <el-table border>
                <el-table-column type="selection" width="80px" align="center"></el-table-column>
                <el-table-column label="图片"></el-table-column>
                <el-table-column label="名称"></el-table-column>
                <el-table-column label="操作"></el-table-column>
            </el-table>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" size="default">保存</el-button>
            <el-button type="primary" size="default" @click="cancel">取消</el-button>
        </el-form-item>
    </el-form>
</template>

<script setup lang="ts">
let $emit=defineEmits(['changeScene'])
// 取消按钮的回调
const cancel=()=>{
    $emit('changeScene',{flag:0,params:''})
}
</script>

<style scoped>

</style>
2.获取添加sku数据展示

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

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

src/views/product/spu/skuForm.vue

<template>
    <el-form label-width="100px">
        <el-form-item label="SKU名称">
            <el-input placeholder="SKU名称" v-model="skuParams.skuName"></el-input>
        </el-form-item>
        <el-form-item label="价格(元)">
            <el-input placeholder="价格(元)" type="number" v-model="skuParams.price"></el-input>
        </el-form-item>
        <el-form-item label="重量(g)">
            <el-input placeholder="重量(g)" type="number" v-model="skuParams.weight"></el-input>
        </el-form-item>
        <el-form-item label="SKU描述">
            <el-input placeholder="SKU描述" type="textarea" v-model="skuParams.skuDesc"></el-input>
        </el-form-item>
        <el-form-item label="平台属性">
            <el-form :inline="true">
                <el-form-item v-for="(item, index) in attrArr" :key="item.id" :label="item.attrName">
                    <el-select v-model="item.attrIdAndValueId">
                        <el-option :value="`${item.id}:${attrValue.id}`" v-for="(attrValue, index) in item.attrValueList"
                            :key="attrValue.id" :label="attrValue.valueName"></el-option>
                    </el-select>
                </el-form-item>
            </el-form>
        </el-form-item>
        <el-form-item label="销售属性">
            <el-form :inline="true">
                <el-form-item v-for="(item, index) in saleArr" :key="item.id" :label="item.saleAttrName">
                    <el-select v-model="item.saleIdAndValueId">
                        <el-option :value="`${item.id}:${saleAttrValue.id}`"
                            v-for="(saleAttrValue, index) in item.spuSaleAttrValueList" :key="saleAttrValue.id"
                            :label="saleAttrValue.saleAttrValueName"></el-option>
                    </el-select>
                </el-form-item>
            </el-form>
        </el-form-item>
        <el-form-item label="图片名称">
            <el-table border :data="imgArr" ref="table">
                <el-table-column type="selection" width="80px" align="center"></el-table-column>
                <el-table-column label="图片">
                    <template #="{ row, $index }">
                        <img :src="row.imgUrl" alt="" style="width: 100px;height: 100px;">
                    </template>
                </el-table-column>
                <el-table-column label="名称" prop="imgName"></el-table-column>
                <el-table-column label="操作">
                    <template #="{ row, $index }">
                        <el-button type="warning" size="small" @click="handler(row)">设置默认</el-button>
                    </template>
                </el-table-column>
            </el-table>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" size="default" @click="save">保存</el-button>
            <el-button size="default" @click="cancel">取消</el-button>
        </el-form-item>
    </el-form>
</template>
 
<script setup lang="ts">
import { reqAttr } from '@/api/product/attr'
import { reqSpuImageList, reqSpuHasSaleAttr, reqAddSku } from '@/api/product/spu'
import { SkuData } from '@/api/product/spu/type';
import { ElMessage } from 'element-plus';
import { reactive, ref } from 'vue';
//自定义事件的方法
let $emit = defineEmits(['changeScene'])
//平台属性
const attrArr = ref<any>([])
//销售属性
const saleArr = ref<any>([])
//照片的数据
const imgArr = ref<any>([])
// 获取table组件实例
const table = ref<any>()
//收集SKU的参数
let skuParams = reactive<SkuData>({
    //父组件传递过来的数据
    category3Id: "",//三级分类的ID
    spuId: "",//已有的SPU的ID
    tmId: "",//SPU品牌的ID
    //v-model收集
    skuName: "",//sku名字
    price: "",//sku价格
    weight: "",//sku重量
    skuDesc: "",//sku的描述
 
    skuAttrValueList: [//平台属性的收集
    ],
    skuSaleAttrValueList: [//销售属性
    ],
    skuDefaultImg: "",//sku图片地址
})
const initSkuData = async (c1Id: string | number, c2Id: string | number, spu: any) => {
    //收集数据
    skuParams.category3Id = spu.category3Id
    skuParams.spuId = spu.id
    skuParams.tmId = spu.tmId
    //获取平台属性
    let result: any = await reqAttr(c1Id, c2Id, spu.category3Id)
    //获取对应的销售属性
    let result1: any = await reqSpuHasSaleAttr(spu.id)
    //获取照片墙的数据
    let result2: any = await reqSpuImageList(spu.id)
    //平台属性
    attrArr.value = result.data
    //销售属性
    saleArr.value = result1.data
    //图片
    imgArr.value = result2.data
}
//对外暴露方法
defineExpose({ initSkuData })
// 取消按钮的回调
const cancel = () => {
    $emit('changeScene', { flag: 0, params: '' })
}
 
//设置默认图片的方法回调
const handler = (row: any) => {
    //点击的时候,全部图片的的复选框不勾选
    imgArr.value.forEach((item: any) => {
        table.value.toggleRowSelection(item, false)
    })
    //选中的图片才勾选
    table.value.toggleRowSelection(row, true)
    //收集图片地址
    skuParams.skuDefaultImg = row.imgUrl
}
 
//保存按钮的方法
const save = async () => {
    //整理参数
    //平台属性
    skuParams.skuAttrValueList = attrArr.value.reduce((prev: any, next: any) => {
        if (next.attrIdAndValueId) {
            let [attrId, valueId] = next.attrIdAndValueId.split(':')
            prev.push({
                attrId,
                valueId
            })
        }
        return prev
    }, [])
    //销售属性
    skuParams.skuSaleAttrValueList = saleArr.value.reduce((prev: any, next: any) => {
        if (next.saleIdAndValueId) {
            let [saleAttrId, saleAttrValueId] = next.saleIdAndValueId.split(':')
            prev.push({
                saleAttrId,
                saleAttrValueId
            })
        }
        return prev
    }, [])
    //添加SKU的请求
    let result: any = await reqAddSku(skuParams)
    if (result.code === 200) {
        ElMessage({
            type: 'success',
            message: '添加SKU成功'
        })
        //通知父组件切换场景为零
        $emit('changeScene', { flag: 0, params: '' })
    } else {
        ElMessage({
            type: 'error',
            message: '添加SKU失败'
        })
    }
}
</script>
 
<style scoped></style>

 六、SKU管理模块

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

① 接口定义 

 src/api/product/sku/index.ts

// SKU模块接口管理
import request from '@/utils/request'
import type { SkuResponseData, SkuInfoData } from './type'
// 枚举地址
enum API {
  // 获取已有的商品的数据-SKU
  SKU_URL = '/admin/product/list/',
  // 上架
  SALE_URL = '/admin/product/onSale/',
  // 下架
  CANCELSALE_URL = '/admin/product/cancelSale/',
  //获取商品详情的接口
  SKUINFO_URL = '/admin/product/getSkuInfo/',
  //删除已有的商品
  DELETESKU_URL = '/admin/product/deleteSku/',
}
// 获取商品SKU的接口
export const reqHasSku = (page: number, limit: number) =>
  request.get<any, SkuResponseData>(API.SKU_URL + `${page}/${limit}`)
// 已有商品上架请求
export const reqSaleSku = (skuId: number) =>
  request.get<any, any>(API.SALE_URL + skuId)
// 下架的请求
export const reqCancelSaleSku = (skuId: number) =>
  request.get<any, any>(API.CANCELSALE_URL + skuId)
//获取商品详情的接口
export const reqSkuInfo = (skuId: number) =>
  request.get<any, SkuInfoData>(API.SKUINFO_URL + skuId)
//删除某一个已有的商品
export const reqRemoveSku = (skuId: number) =>
  request.delete<any, any>(API.DELETESKU_URL + skuId)

② 数据ts类型定义

src/api/product/sku/type.ts

export interface ResponseData {
  code: number
  message: string
  ok: boolean
}
//定义SKU对象的ts类型
export interface Attr {
  id?: number
  attrId: number | string //平台属性的ID
  valueId: number | string //属性值的ID
}
export interface saleArr {
  id?: number
  saleAttrId: number | string //属性ID
  saleAttrValueId: number | string //属性值的ID
}
export interface SkuData {
  category3Id?: string | number //三级分类的ID
  spuId?: string | number //已有的SPU的ID
  tmId?: string | number //SPU品牌的ID
  skuName?: string //sku名字
  price?: string | number //sku价格
  weight?: string | number //sku重量
  skuDesc?: string //sku的描述
  skuAttrValueList?: Attr[]
  skuSaleAttrValueList?: saleArr[]
  skuDefaultImg?: string //sku图片地址
  isSale?: number //控制商品的上架与下架
  id?: number
}
 
//获取SKU接口返回的数据ts类型
export interface SkuResponseData extends ResponseData {
  data: {
    records: SkuData[]
    total: number
    size: number
    current: number
    orders: []
    optimizeCountSql: boolean
    hitCount: boolean
    countId: null
    maxLimit: null
    searchCount: boolean
    pages: number
  }
}
 
//获取SKU商品详情接口的ts类型
export interface SkuInfoData extends ResponseData {
  data: SkuData
}

 3.业务实现

src/views/product/sku/index.vue  

<template>
  <el-card>
    <el-table border style="margin: 10px" :data="skuAttr">
      <el-table-column
        label="序号"
        type="index"
        align="center"
        width="80px"
      ></el-table-column>
      <el-table-column
        label="名称"
        show-overflow-tooltip
        width="150px"
        prop="skuName"
      ></el-table-column>
      <el-table-column
        label="描述"
        show-overflow-tooltip
        width="150px"
        prop="skuDesc"
      ></el-table-column>
      <el-table-column label="图片" width="150px">
        <template #="{ row, $index }">
          <img
            :src="row.skuDefaultImg"
            alt=""
            style="width: 100px; height: 100px"
          />
        </template>
      </el-table-column>
      <el-table-column
        label="重量"
        width="150px"
        prop="weight"
      ></el-table-column>
      <el-table-column
        label="价格"
        width="150px"
        prop="price"
      ></el-table-column>
      <el-table-column label="操作" width="280px" fixed="right">
        <template #="{ row, $index }">
          <el-button
            :type="row.isSale === 1 ? 'info' : 'success'"
            size="small"
            :icon="row.isSale === 1 ? 'Bottom' : 'Top'"
            @click="updateSale(row)"
          ></el-button>
          <el-button
            type="primary"
            size="small"
            icon="Edit"
            @click="updateSku"
          ></el-button>
          <el-button
            type="info"
            size="small"
            icon="InfoFilled"
            @click="findSku(row)"
          ></el-button>
          <el-popconfirm
            :title="`你确定要删除${row.skuName}?`"
            width="200px"
            @confirm="removeSku(row.id)"
          >
            <template #reference>
              <el-button type="danger" size="small" icon="Delete"></el-button>
            </template>
          </el-popconfirm>
        </template>
      </el-table-column>
    </el-table>
    <el-pagination
      v-model:current-page="pageNo"
      v-model:page-size="pageSize"
      :page-sizes="[10, 20, 30, 40]"
      :background="true"
      layout="prev, pager, next, jumper,->,sizes,total"
      :total="total"
      @current-change="getHasSku"
      @size-change="handler"
    />
    <!-- 抽屉组件:展示商品详情 -->
    <el-drawer v-model="drawer">
      <!-- 标题部分 -->
      <template #header>
        <h4>查看商品详情</h4>
      </template>
      <template #default>
        <el-row style="margin: 10px 0">
          <el-col :span="6">名称</el-col>
          <el-col :span="18">{{ skuInfo.skuName }}</el-col>
        </el-row>
        <el-row style="margin: 10px 0">
          <el-col :span="6">描述</el-col>
          <el-col :span="18">{{ skuInfo.skuDesc }}</el-col>
        </el-row>
        <el-row style="margin: 10px 0">
          <el-col :span="6">价格</el-col>
          <el-col :span="18">{{ skuInfo.price }}</el-col>
        </el-row>
        <el-row style="margin: 10px 0">
          <el-col :span="6">平台属性</el-col>
          <el-col :span="18">
            <el-tag
              style="margin: 5px"
              type="danger"
              v-for="item in skuInfo.skuAttrValueList"
              :key="item.id"
            >
              {{ item.valueName }}
            </el-tag>
          </el-col>
        </el-row>
        <el-row style="margin: 10px 0">
          <el-col :span="6">销售属性</el-col>
          <el-col :span="18">
            <el-tag
              style="margin: 5px"
              type="success"
              v-for="item in skuInfo.skuSaleAttrValueList"
              :key="item.id"
            >
              {{ item.saleAttrValueName }}
            </el-tag>
          </el-col>
        </el-row>
        <el-row style="margin: 10px 0">
          <el-col :span="6">商品图片</el-col>
          <el-col :span="18">
            <el-carousel :interval="4000" type="card" height="200px">
              <el-carousel-item
                v-for="item in skuInfo.skuImageList"
                :key="item.id"
              >
                <img :src="item.imgUrl" style="width: 100%; height: 100%" />
              </el-carousel-item>
            </el-carousel>
          </el-col>
        </el-row>
      </template>
    </el-drawer>
  </el-card>
</template>
 
<script setup lang="ts">
import {
  reqCancelSaleSku,
  reqHasSku,
  reqRemoveSku,
  reqSaleSku,
  reqSkuInfo,
} from '@/api/product/sku'
import { SkuData, SkuInfoData, SkuResponseData } from '@/api/product/sku/type'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
// 分页器当前页码
let pageNo = ref<number>(1)
// 每一页展示几条数据
let pageSize = ref<number>(10)
let total = ref<number>(0)
let skuAttr = ref<SkuData[]>([])
//控制抽屉显示与隐藏的字段
let drawer = ref<boolean>(false)
let skuInfo = ref<any>({})
// 组件挂载完毕
onMounted(() => {
  getHasSku()
})
const getHasSku = async (pager = 1) => {
  //当前分页器的页码
  pageNo.value = pager
  let result: SkuResponseData = await reqHasSku(pageNo.value, pageSize.value)
  if (result.code === 200) {
    total.value = result.data.total
    skuAttr.value = result.data.records
  }
}
// 分页器下拉菜单发生变化触发
const handler = (pageSize: number) => {
  getHasSku()
}
// 商品的上架与下架的操作
const updateSale = async (row: SkuData) => {
  //如果当前商品的isSale==1,说明当前商品是上架的额状态->更新为下架
  //否则else情况与上面情况相反
  if (row.isSale === 1) {
    // 下架操作
    await reqCancelSaleSku(row.id as number)
    // 提示信息
    ElMessage({
      type: 'success',
      message: '下架成功',
    })
  } else {
    // 上架操作
    await reqSaleSku(row.id as number)
    // 提示信息
    ElMessage({
      type: 'success',
      message: '上架成功',
    })
  }
  //发请求获取当前更新完毕的全部已有的SKU
  getHasSku(pageNo.value)
}
// 更新已有的SKU
const updateSku = () => {
  ElMessage({
    type: 'success',
    message: '程序员在努力的更新中......',
  })
}
//查看商品详情按钮的回调
const findSku = async (row: SkuData) => {
  //抽屉展示出来
  drawer.value = true
  //获取已有商品详情数据
  let result: SkuInfoData = await reqSkuInfo(row.id as number)
  if (result.code === 200) {
    //存储已有的SKU
    skuInfo.value = result.data
  }
}
//删除某一个已有的商品
const removeSku = async (id: number) => {
  //删除某一个已有商品的情况
  let result: any = await reqRemoveSku(id)
  if (result.code === 200) {
    //提示信息
    ElMessage({
      type: 'success',
      message: '删除成功',
    })
    //获取已有全部商品
    getHasSku(pageNo.value > 1 ? pageNo.value : pageNo.value - 1)
  } else {
    ElMessage({
      type: 'error',
      message: '系统数据不能删除',
    })
  }
}
</script>
 
<style scoped>
.el-carousel__item h3 {
  color: #475669;
  opacity: 0.75;
  line-height: 200px;
  margin: 0;
  text-align: center;
}
 
.el-carousel__item:nth-child(2n) {
  background-color: #99a9bf;
}
 
.el-carousel__item:nth-child(2n + 1) {
  background-color: #d3dce6;
}
</style>

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何测试响应式网站

我们每天通过多种设备访问互联网。移动电话&#xff0c;台式机/笔记本电脑&#xff0c;平板电脑&#xff0c;平板电脑…我们所掌握的设备数量已经增长为天文数字。作为消费者&#xff0c;体验很棒。我们可以随时随地在任何设备上自由访问互联网。但对于Web开发人员&#xff0c;…

磁盘格式化文件恢复:一文看懂数据恢复操作

当你意识到关键的硬盘已经被格式化&#xff0c;而且你不能获取里面的内容时&#xff0c;这会是非常令人沮丧的。这种情况可能是因为硬盘被不小心格式化&#xff0c;或者是你在试图修正一些问题、调整文件系统或者释放存储空间时&#xff0c;有意进行的格式化。无论具体情况是什…

Go 语言变量

变量来源于数学&#xff0c;是计算机语言中能储存计算结果或能表示值抽象概念。 变量可以通过变量名访问。 Go 语言变量名由字母、数字、下划线组成&#xff0c;其中首个字符不能为数字。 声明变量的一般形式是使用 var 关键字&#xff1a; var identifier type 可以一次声…

线程基础知识

进程是资源分配的最小单位&#xff0c;线程是程序执行的最小单位… 为什么使用线程 多线程之间会共享同一块地址空间和所有可用数据的能力&#xff0c;这是进程所不具备的线程要比进程更轻量级 &#xff0c;由于线程更轻&#xff0c;所以它比进程(fork创建进程以执行新的任务…

Postgresql 从小白到高手 十一 :数据迁移ETL方案

文章目录 Postgresql 数据迁移ETL方案1、Pg 同类型数据库2 、Pg 和 不同数据库 Postgresql 数据迁移ETL方案 1、Pg 同类型数据库 备份 : pg_dump -U username -d dbname -f backup.sql插入数据&#xff1a; psql -U username -d dbname -f backup.sqlpg_restore -U username…

远程桌面连接服务器怎样连接不上的六个常见原因

远程桌面连接服务器无法连接的问题可能由多种原因引起。以下是一些常见的问题及其解决方案&#xff1a; 1. 网络连接问题&#xff1a;远程桌面连接的基础是稳定的网络连接。如果网络连接不稳定或中断&#xff0c;那么你将无法连接到远程桌面。检查你的网络连接&#xff0c;确保…

Codigger数据篇(中):数据可控性的灵活配置

在数据服务领域中&#xff0c;数据可控性无疑是至关重要的一环。数据可控性不仅关乎数据的安全性和隐私性&#xff0c;更直接影响到数据价值的实现。Codigger&#xff0c;在其数据可控性方面的灵活配置&#xff0c;为用户提供了更加便捷、高效的数据管理体验。 一、自主选择数…

Spring6 当中 Bean 的生命周期的详细解析:有五步,有七步,有十步

1. Spring6 当中 Bean 的生命周期的详细解析&#xff1a;有五步&#xff0c;有七步&#xff0c;有十步 文章目录 1. Spring6 当中 Bean 的生命周期的详细解析&#xff1a;有五步&#xff0c;有七步&#xff0c;有十步每博一文案1.1 什么是 Bean 的生命周期1.2 Bean 的生命周期 …

ThinkPHP Lang多语言本地文件包含漏洞(QVD-2022-46174)漏洞复现

1 漏洞描述 ThinkPHP是一个在中国使用较多的PHP框架。在其6.0.13版本及以前&#xff0c;存在一处本地文件包含漏洞。当ThinkPHP开启了多语言功能时&#xff0c;攻击者可以通过lang参数和目录穿越实现文件包含&#xff0c;当存在其他扩展模块如 pear 扩展时&#xff0c;攻击者可…

esp32学习

开启自动补全功能 Arduino IDE 2.0开启代码补全及修改中文_arduino ide怎么设置中文-CSDN博客 PWM 、 ADC转换 在使用这个adc默认配置的时候adc引脚的输入电压必须是介于0-1之间&#xff0c;如何高于1v的电压都会视为一个最高值&#xff0c;如果要增加测量电压你就需要配置一…

【JAVA】part5-Java集合

Java 集合 Java集合概述 Java数组的局限性 数组初始化后大小不可变&#xff1b;数组只能按索引顺序存取。 Java的java.util包主要提供了以下三种类型的集合&#xff1a; List&#xff1a;一种有序列表的集合&#xff0c;例如&#xff0c;按索引排列的Student的List&#xff1b…

车载气象站:可移动监测的气象站

TH-CZ5车载气象站是一种专门针对车辆、船舶等应急环境检测设备而设计的可移动监测的气象站。 一、系统介绍 车载气象站系统采用先进的高精度GPS及三轴电子罗盘&#xff0c;可实现车行驶时的风速、风向检测。整机为野外型设计&#xff0c;同时还可对气温、相对湿度、雨量、气压…

Linux修改文件权限命令 chmod

【例子引入】 以下面命令为例&#xff1a; chmod 777 Random.py 当写入下面名为Random.py的代码后&#xff1a; 如果直接运行&#xff0c;会显示权限不够 当输入 chmod 777 Random.py 更改权限后&#xff0c;才能够正常运行 在终端中输入 这条命令是关于Linux或Unix-like系…

FlaUI

FlaUI是一个基于微软UIAutomation技术&#xff08;简称UIA&#xff09;的.NET库&#xff0c;它主要用于对Windows应用程序&#xff08;如Win32、WinForms、WPF、Store Apps等&#xff09;进行自动化UI测试。FlaUI的前身是TestStack.White&#xff0c;由Roemer开发&#xff0c;旨…

Socket编程--TCP连接以及并发处理

流程图 网络传输流程&#xff1a; TCP连接&#xff1a; api 客户端&#xff1a; socket: 创建套接字 domain: AF_INET &#xff1a;IPv4 type: SOCK_STREAM(tcp)、SOCK_DGRAM&#xff08;udp&#xff09; protocol: 0 默认协议 返回值&#xff1a;成功返回一个新的套接字…

Linux-进程间通信(进程间通信介绍、匿名管道原理及代码使用、命名管道原理及代码使用)

一、进程通信介绍 1.1进程间通信的目的 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程资源共享&#xff1a;多个进程之间共享同样的资源。通知事件&#xff1a;一个进程需要向另一个或一组进程发送消息&#xff0c;通知它&#xff08;它们&#xff09;发生了某…

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

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

【Unity动画系统】Animator组件的属性

介绍Animator组件的全部属性 Controller&#xff1a;动画控制器 Avatar&#xff1a;人物骨骼 Apply Root Motion&#xff1a;有一些动画片段自带位移&#xff0c;如果希望自带的位移应用在游戏对象上&#xff0c;那么就勾选&#xff1b;如果自己编写脚本&#xff0c;那么就不…

如何用智能获客开启新商机?揭秘赢销侠软件的奇效

在当今数字化竞争日益激烈的商业环境中&#xff0c;企业为了生存和发展&#xff0c;必须寻找新的途径以获取潜在客户。智能获客作为一种新型的营销方式&#xff0c;正以其高效、精准的特点改变着传统的市场开拓模式。而在这个过程中&#xff0c;自动获客软件的作用愈发凸显&…

LLM大语言模型原理、发展历程、训练方法、应用场景和未来趋势

LLM&#xff0c;全称Large Language Model&#xff0c;即大型语言模型。LLM是一种强大的人工智能算法&#xff0c;它通过训练大量文本数据&#xff0c;学习语言的语法、语义和上下文信息&#xff0c;从而能够对自然语言文本进行建模。这种模型在自然语言处理&#xff08;NLP&am…