硅谷甄选项目笔记

探索性——学习Vue3

2023.8.4-2023.8.

一、再次学习ES6文档

1.2 ES6 环境搭建 | 菜鸟教程 (runoob.com)

二、再次学习Vue3文档

创建一个 Vue 应用 | Vue.js (vuejs.org)

三、全新技术栈

vue3+TS+pinia+element-plus+echarts

尚硅谷vue3+TS项目

克隆地址:https://gitee.com/jch1011/vue3_communication.git

笔记:《硅谷甄选》项目笔记_星辰大海1412的博客-CSDN博客

尚硅谷硅谷甄选笔记_予倾的博客-CSDN博客

文档:http://139.198.104.58:8209/swagger-ui.html

http://139.198.104.58:8212/swagger-ui.html#/

  • 疑问:原生DOM事件和自定义事件在vue2和3的区别:https://blog.csdn.net/qyl_0316/article/details/117415608

四、项目部署

一项目初始化

问题一:爆红

根据报错信息,问题出现在您的 “main.ts” 文件中,找不到模块 “vue”。报错信息建议您将 “moduleResolution” 选项设置为 “node” 或者将别名添加到 “paths” 选项中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请按照以下步骤解决问题:

  1. 确保您已经在项目根目录中安装了 Vue.js 依赖。如果尚未安装,请运行以下命令进行安装:

    npm install vue@next
    
  2. 打开 “main.ts” 文件,并确保您正确导入 Vue 模块。请修改导入语句如下:

    import { createApp } from 'vue';
    import App from './App.vue';
    
    createApp(App).mount('#app');
    

    请注意,根据您的项目结构,可能需要调整 “./App.vue” 的路径。

  3. 确保您的 “tsconfig.json” 和tsconfig.node.json文件中的配置项正确。以下是配置:

//修改tsconfig.node.json文件:
{
    
    
  "compilerOptions": {
    
    
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "node",//这里修改位node
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./",//添加这一句
    "paths": {
    
    
      "vue": ["node_modules/vue/dist/vue.esm-bundler.js"],
      "vite": ["node_modules/vite/dist/index.js"]
    }//添加这一句
  },
  "include": ["vite.config.ts"]
}
//修改tsconfig.js
{
    
    
  "compilerOptions": {
    
    
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "node",//修改为node
    "allowImportingTsExtensions": true,//删除这一句
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{
    
     "path": "./tsconfig.node.json" }]
}

这里的配置选项可以根据您的项目需求进行调整。确保 “target” 和 “module” 设置为 “ESNext”,并且 “moduleResolution” 设置为 “node”。

4.保存就没有爆红了

二项目配置

1.配置eslint和prettier

目的:为了使检查代码语法规范和发现错误

eslint和prettier这俩兄弟一个保证js代码质量,一个保证代码美观

npm install -D eslint-plugin-import eslint-plugin-vue eslint-plugin-node eslint-plugin-prettier eslint-config-prettier eslint-plugin-node @babel/eslint-parser

npm install -D eslint-plugin-prettier prettier eslint-config-prettier
2.配置stylelint

stylelint 为 css的lint工具。可格式化css代码,检查css语法错误与不合理的写法,指定css书写顺序等

npm add sass sass-loader stylelint postcss postcss-scss postcss-html stylelint-config-prettier stylelint-config-recess-order stylelint-config-recommended-scss stylelint-config-standard stylelint-config-standard-vue stylelint-scss stylelint-order stylelint-config-standard-scss -D --legacy-peer-deps
3.配置husky

用于远程提交规范格式(提交到github)

安装husky

npm install -D husky

会在根目录下生成个一个.husky目录,在这个目录下面会有一个pre-commit文件,这个文件里面的命令在我们执行commit的时候就会执行

在.husky/pre-commit文件添加如下命令:

npx husky-init

会在根目录下生成个一个.husky目录,在这个目录下面会有一个pre-commit文件,这个文件里面的命令在我们执行commit的时候就会执行

在.husky/pre-commit文件添加如下命令:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm run format
og.csdn.net/m0_61662775/article/details/130781258
4.配置commitlint

对于我们的commit信息,也是有统一规范的,不能随便写,要让每个人都按照统一的标准来执行,我们可以利用commitlint来实现。

5.统一包管理工具

团队开发项目的时候,需要统一包管理器工具,因为不同包管理器工具下载同一个依赖,可能版本不一样,

导致项目出现bug问题,因此包管理器工具需要统一管理!!!

在根目录创建 scritps/preinstall.js 文件,添加下面的内容

if (!/npm/.test(process.env.npm_execpath || '')) {
  console.warn(
    `\u001b[33mThis repository must using pnpm as the package manager ` +
    ` for scripts to work properly.\u001b[39m\n`,
  )
  process.exit(1)
}

配置命令

"scripts": {
    "preinstall": "node ./scripts/preinstall.js"
}

当使用npm或者yarn来安装包的时候,就会报错了。原理就是在install的时候会触发preinstall(npm提供的生命周期钩子)这个文件里面的代码。

三项目集成

1.element-plus

官网:安装 | Element Plus (element-plus.org)

2.src别名配置

在开发项目的时候文件与文件关系可能很复杂,因此我们需要给src文件夹配置一个别名!!!

// vite.config.ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
    plugins: [vue()],
    resolve: {
        alias: {
            "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
        }
    }
})
TypeScript 编译配置

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { //路径映射,相对于baseUrl
      "@/*": ["src/*"] 
    }
  }
}
3.环境配置

项目开发过程中,至少会经历开发环境、测试环境和生产环境(即正式环境)三个阶段。不同阶段请求的状态(如接口地址等)不尽相同,若手动切换接口地址是相当繁琐且易出错的。于是环境变量配置的需求就应运而生,我们只需做简单的配置,把环境状态切换的工作交给代码。

开发环境(development)

顾名思义,开发使用的环境,每位开发人员在自己的dev分支上干活,开发到一定程度,同事会合并代码,进行联调。

测试环境(testing)

测试同事干活的环境啦,一般会由测试同事自己来部署,然后在此环境进行测试

生产环境(production)

生产环境是指正式提供对外服务的,一般会关掉错误报告,打开错误日志。(正式提供给客户使用的环境。)

注意:一般情况下,一个环境对应一台服务器,也有的公司开发与测试环境是一台服务器!!!

项目根目录分别添加 开发、生产和测试环境的文件!

.env.development
.env.production
.env.test

文件内容

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/dev-api'
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'production'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/prod-api'
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = '硅谷甄选运营平台'
VITE_APP_BASE_API = '/test-api'
#配置运行命令:package.json
 "scripts": {
    
    
    "dev": "vite --open",
    "build:test": "vue-tsc && vite build --mode test",
    "build:pro": "vue-tsc && vite build --mode production",
    "preview": "vite preview"
  }

通过import.meta.env获取环境变量

4.SVG图标配置

在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后,页面上加载的不再是图片资源,

这对页面性能来说是个很大的提升,而且我们SVG文件比img要小的很多,放在项目中几乎不占用资源

安装SVG依赖插件

pnpm install vite-plugin-svg-icons -D

在vite.config.ts中配置插件

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
export default () => {
  return {
    plugins: [
      createSvgIconsPlugin({
        // Specify the icon folder to be cached
        iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
        // Specify symbolId format
        symbolId: 'icon-[dir]-[name]',
      }),
    ],
  }
}

入口文件导入

import 'virtual:svg-icons-register'

为了方便需要全局注册

在src文件夹目录下创建一个index.ts文件:用于注册components文件夹内部全部全局组件!!!

import SvgIcon from './SvgIcon/index.vue'
import Svg1 from './Svg1/index.vue'

import type { App, Component } from 'vue';
const components: { [name: string]: Component } = { SvgIcon ,Svg1};
//any接收任何对象存在危险
//const allGloablComponent:any={SvgIcon,Svg1}

//对外暴露插件对象
export default {
    install(app: App) {
        Object.keys(components).forEach((key: string) => {
            app.component(key, components[key]);
        })
    }
}

在入口文件引入src/index.ts文件,通过app.use方法安装自定义插件

import globalComponent from '@/components'
app.use(globalComponent)
5.集成sass

我们目前在组件内部已经可以使用scss样式,因为在配置styleLint工具的时候,项目当中已经安装过sass sass-loader,因此我们再组件内可以使用scss语法!!!需要加上lang=“scss”

接下来我们为项目添加一些全局的样式

在src/styles目录下创建一个index.scss文件,当然项目中需要用到清除默认样式,因此在index.scss引入reset.scss

@import reset.scss

在入口文件引入

import '@/styles'

但是你会发现在src/styles/index.scss全局样式文件中没有办法使用变量 . 因此需要给项目中引入全局变量 变量.因此需要给项目中引入全局变量变量.因此需要给项目中引入全局变量.

在style/variable.scss创建一个variable.scss文件!

在vite.config.ts文件配置如下:

export default defineConfig((config) => {
	css: {
      preprocessorOptions: {
        scss: {
          javascriptEnabled: true,
          additionalData: '@import "./src/styles/variable.scss";',
        },
      },
    },
	}
})

@import “./src/styles/variable.less”;后面的;不要忘记,不然会报错!

配置完毕你会发现scss提供这些全局变量可以在组件样式中使用了!!!

6.mock配置

安装依赖:https://www.npmjs.com/package/vite-plugin-mock

npm install -D vite-plugin-mock [email protected]
在 vite.config.js 配置文件启用插件。

import {
    
     UserConfigExport, ConfigEnv } from 'vite'
import {
    
     viteMockServe } from 'vite-plugin-mock'
import vue from '@vitejs/plugin-vue'
export default ({
    
     command })=> {
    
    
  return {
    
    
    plugins: [
      vue(),
      viteMockServe({
    
    
        localEnabled: command === 'serve',
      }),
    ],
  }
}

在根目录创建mock文件夹:去创建我们需要mock数据与接口!!!

在mock文件夹内部创建一个user.ts文件

//用户信息数据
function createUserList() {
    
    
    return [
        {
    
    
            userId: 1,
            avatar:
                'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
            username: 'admin',
            password: '111111',
            desc: '平台管理员',
            roles: ['平台管理员'],
            buttons: ['cuser.detail'],
            routes: ['home'],
            token: 'Admin Token',
        },
        {
    
    
            userId: 2,
            avatar:
                'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
            username: 'system',
            password: '111111',
            desc: '系统管理员',
            roles: ['系统管理员'],
            buttons: ['cuser.detail', 'cuser.user'],
            routes: ['home'],
            token: 'System Token',
        },
    ]
}

export default [
    // 用户登录接口
    {
    
    
        url: '/api/user/login',//请求地址
        method: 'post',//请求方式
        response: ({
     
      body }) => {
    
    
            //获取请求体携带过来的用户名与密码
            const {
    
     username, password } = body;
            //调用获取用户信息函数,用于判断是否有此用户
            const checkUser = createUserList().find(
                (item) => item.username === username && item.password === password,
            )
            //没有用户返回失败信息
            if (!checkUser) {
    
    
                return {
    
     code: 201, data: {
    
     message: '账号或者密码不正确' } }
            }
            //如果有返回成功信息
            const {
    
     token } = checkUser
            return {
    
     code: 200, data: {
    
     token } }
        },
    },
    // 获取用户信息
    {
    
    
        url: '/api/user/info',
        method: 'get',
        response: (request) => {
    
    
            //获取请求头携带token
            const token = request.headers.token;
            //查看用户信息是否包含有次token用户
            const checkUser = createUserList().find((item) => item.token === token)
            //没有返回失败的信息
            if (!checkUser) {
    
    
                return {
    
     code: 201, data: {
    
     message: '获取用户信息失败' } }
            }
            //如果有返回成功信息
            return {
    
     code: 200, data: {
    
    checkUser} }
        },
    },
]

7.axios配置

在开发项目的时候避免不了与后端进行交互,因此我们需要使用axios插件实现发送网络请求。在开发项目的时候

我们经常会把axios进行二次封装。

目的:

1:使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)

2:使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)

在根目录下创建utils/request.ts

import axios from "axios";
import {
    
     ElMessage } from "element-plus";
//创建axios实例
let request = axios.create({
    
    
    baseURL: import.meta.env.VITE_APP_BASE_API,
    timeout: 5000
})
//请求拦截器
request.interceptors.request.use(config => {
    
    
    return config;
});
//响应拦截器
request.interceptors.response.use((response) => {
    
    
    return response.data;
}, (error) => {
    
    
    //处理网络错误
    let msg = '';
    let status = error.response.status;
    switch (status) {
    
    
        case 401:
            msg = "token过期";
            break;
        case 403:
            msg = '无权访问';
            break;
        case 404:
            msg = "请求地址错误";
            break;
        case 500:
            msg = "服务器出现问题";
            break;
        default:
            msg = "无网络";

    }
    ElMessage({
    
    
        type: 'error',
        message: msg
    })
    return Promise.reject(error);

});
export default request;
8.统一API的配置

3.8API接口统一管理
在开发项目的时候,接口可能很多需要统一管理。在src目录下去创建api文件夹去统一管理项目的接口;

//api文件下的index.ts
//统一管理咱们项目用户相关的接口

import request from '@/utils/request'

import type {

 loginFormData,

 loginResponseData,

 userInfoReponseData,

} 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, userInfoReponseData>(API.USERINFO_URL)

//退出登录

export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)

//api文件下的type.ts
//登录需要携带的参数ts类型

export interface loginForm{
    username:string,
    password:string
}

interface dataType{
    token:string
}
//登录接口返回数据类型

export interface loginResponeData{
    code:number,
    data:dataType
}

interface userInfo{

    userId:number,
    avatar:string,
    username:string,
    password:string,
    desc:string,
    roles:string[],
    buttons:string[],
    routes:string[],
    token:string

}
interface user{
    checkUser:userInfo
}
export interface userResponeData{
    code:number,
    data:user
}
9.配置路由

需要至少四个一级路由:主页、登入页、404页、任意路由(指向404)

安装路由插件:4版本

pnpm i vue-router

src下新建文件夹views

分别创建404、login、home路由组件

src下新建router文件夹—包含index.ts和routes.ts

src/router/routes.ts

// 对外暴露配置路由(常量路由)
export const constantRoute = [
    {
        name: 'login',// 命名路由--做权限会用到
        path: '/login',
        component: () => import('@/views/login/index.vue')
    },
    {
        // 登入成功后展示数据的路由
        name: 'home',
        path: '/',
        component: () => import('@/views/home/index.vue')
    },
    {
        // 404路由
        name: '404',
        path: '/404',
        component: () => import('@/views/404/index.vue')
    },
    {
        // 任意路由
        name: 'any',
        path: '/:pathMatch(.*)*',
        redirect: '/404'
    }
]
src/router/index.ts

// 通过 vue-router 插件实现模板路由配置
import { createRouter, createWebHashHistory } from "vue-router";
import { constantRoute } from "./routes";

// 创建路由器
let router = createRouter({
    // 路由模式hash
    history: createWebHashHistory(),
    routes: constantRoute,
    // 滚动行为
    scrollBehavior() {
        return {
            left: 0,
            top: 0
        }
    }

})

export default router

入口文件中引入路由并注册 src/main.ts

// 引入路由

import router from '@/router'
// 注册模板路由
app.use(router)

一级路由在App组件中展示即可

<router-view></router-view>
10.pinia小仓库配置

首先安装pinia仓库

npm i pinia

新建大仓库文件 src/store/index.ts

import { createPinia } from 'pinia'
// 创建大仓库
let pinia = createPinia()
// 对外暴露:入口文件需要安装大仓库
export default pinia

入口文件中安装大仓库 src/main.ts

import pinia from '@/store'
app.use(pinia)

pinia的使用

//用户相关的小仓库
import { defineStore } from "pinia";
const userUserStore=defineStore('User',{
    state:()=>{
        return{}
    },
    actions:{

    },
    getters:{

    }
})

export default userUserStore;

在vue中引入

//引入小仓库相关用户数据
import useUserStore from '../../store/modules/user'
const useStore=useUserStore();

四静态页面开始

根据路由先进行这些页面的搭建

1.登录页面
1.静态页面搭建(搭框架)
<template>
  <div class="login_container">
    <el-row>
      <el-col :span="12" :xs="0"></el-col>
      <el-col :span="12" :xs="24">
        <el-form class="login_form">
          <h1>Hello</h1>
          <h2>欢迎来到硅谷甄选</h2>
          <el-form-item size="normal">
            <el-input :prefix-icon="User" v-model="loginForm.username" placeholder="请输入账号"></el-input>
          </el-form-item>
          <el-form-item  size="normal">
            <el-input type="password"  :prefix-icon="Lock" v-model="loginForm.password"  placeholder="请输入密码" show-password></el-input>
          </el-form-item>
          <el-form-item  size="normal">
            <el-button type="primary" size="default" class="login_btn">登录</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </div>
</template>

<script setup lang="ts">
import {User,Lock} from '@element-plus/icons-vue'
import {reactive} from'vue'

const loginForm=reactive({
username:'admin',
password:'111111'
})
</script>
<style scoped lang="scss">
.login_container {
  width: 100%;
  //vh 是一种单位,表示视口高度的百分比
  height: 100vh;
  background: url('@/assets/images/background.jpg') no-repeat;
  //  background-size: cover用于创建全屏背景图或需要完全填充元素的背景图的情况。
  background-size: cover;

  .login_form {
    position: relative;
    width: 80%;
    top: 30vh;
    background: url('@/assets/images/login_form.png') no-repeat;
    background-size: cover;
    h1 {
      color: white;
      font-size: 40px;
    }
    h2 {
      color: white;
      font-size: 20px;
      margin: 20px 0px;
    }
    .login_btn{
      width: 100%;
    }
  }
}
</style>
<template>
  <div class="login_container">
    <el-row>
      <el-col :span="12" :xs="0"></el-col>
      <el-col :span="12" :xs="24">
        <el-form class="login_form">
          <h1>Hello</h1>
          <h2>欢迎来到硅谷甄选</h2>
          <el-form-item size="normal">
            <el-input :prefix-icon="User" v-model="loginForm.username" placeholder="请输入账号"></el-input>
          </el-form-item>
          <el-form-item  size="normal">
            <el-input type="password"  :prefix-icon="Lock" v-model="loginForm.password"  placeholder="请输入密码" show-password></el-input>
          </el-form-item>
          <el-form-item  size="normal">
            <el-button type="primary" size="default" class="login_btn">登录</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </div>
</template>

<script setup lang="ts">
import {User,Lock} from '@element-plus/icons-vue'
import {reactive} from'vue'
//收集用户名和密码的数据
let  loginForm=reactive({
username:'admin',
password:'111111'
})
</script>
<style scoped lang="scss">
.login_container {
  width: 100%;
  //vh 是一种单位,表示视口高度的百分比
  height: 100vh;
  background: url('@/assets/images/background.jpg') no-repeat;
  //  background-size: cover用于创建全屏背景图或需要完全填充元素的背景图的情况。
  background-size: cover;

  .login_form {
    position: relative;
    width: 80%;
    top: 30vh;
    background: url('@/assets/images/login_form.png') no-repeat;
    background-size: cover;
    h1 {
      color: white;
      font-size: 40px;
    }
    h2 {
      color: white;
      font-size: 20px;
      margin: 20px 0px;
    }
    .login_btn{
      width: 100%;
    }
  }
}
</style>

2.登录业务封装(搭框架)
 //login
 <el-button  :loading="loading" type="primary" size="default" class="login_btn" @click="login">登录</el-button>


<script setup lang="ts">
import {User,Lock} from '@element-plus/icons-vue'
import {reactive,ref} from'vue'
import {useRouter} from 'vue-router'
import{ElNotification}from 'element-plus'
//引入小仓库相关用户数据
import useUserStore from '../../store/modules/user'
const $router=useRouter();


const useStore=useUserStore();
//收集用户名和密码的数据
let  loginForm=reactive({
username:'admin',
password:'111111'
})
//登录按钮回调
let loading=ref(false)
const login= async()=>{
    loading.value=true
  try{
    //登录成功
   await useStore.userLogin(loginForm);
   //跳转
   $router.push('/')
   ElNotification({
     type:'success',
     message:'登录成功'
   })
   loading.value=false
  }catch(error){
     loading.value=false
    ElNotification({
     type:'error',
     message:(error as Error).message
   })
  
  }
}
</script>
//小仓库user.ts
//用户相关的小仓库

import { defineStore } from "pinia"
//引入接口
import {reqLogin} from '@/api/user'
//引入数据类型
import {loginForm} from '@/api/user/type';
const userUserStore=defineStore('User',{
    state:()=>{
        return{
            token:'',
        }
    },
    actions:{
         async userLogin(data:loginForm){
          const result:any= await reqLogin(data)
          if(result.code===200){
              //pinia存储token
          this.token=result.data.token
          //本地存储持久化
         localStorage.setItem("TOKEN",result.data.token)
                return "ok"

          }else{
              return Promise.reject(new Error(result.data.message))
          }
        }
    },
    getters:{

    }
})

export default userUserStore;
3.用户仓库数据ts类型的定义(细节)

表现在模块化

创建store/modules/types/type.ts代码如下:

//定义小仓库数据state的类型
export interface UserState{
    
    
    token:string|null;
}

创建util/token.ts

//封装本地存储数据和读取数据方法

export const SET_TOKEN=(token:string)=>{
    
    
    localStorage.setItem("TOKEN",token)
}
export const GET_TOKEN=()=>{
    
    
   return  localStorage.getItem("TOKEN")
}

user.ts有修改

//用户相关的小仓库

import {
    
     defineStore } from "pinia"
//引入接口
import {
    
    reqLogin} from '@/api/user'
//引入数据类型
import {
    
    loginForm,loginResponeData} from '@/api/user/type';
import {
    
    UserState} from './types/type';
//引入本地存储的操作
import {
    
     SET_TOKEN ,GET_TOKEN} from "@/util/token";

const userUserStore=defineStore('User',{
    
    
    state:():UserState=>{
    
    
        return{
    
    
            token: GET_TOKEN()
        }
    },
    actions:{
    
    
         async userLogin(data:loginForm){
    
    
          const result:loginResponeData= await reqLogin(data)
          if(result.code===200){
    
    
              //pinia存储token
          this.token=(result.data.token as string)
          //本地存储持久化
        SET_TOKEN((result.data.token as string))
                return "ok"

          }else{
    
    
              return Promise.reject(new Error(result.data.message))
          }
        }
    },
    getters:{
    
    

    }
})

export default userUserStore;

api/type.ts有修改


interface dataType{
    
    
    token?:string,
    message?:string
}
//登录接口返回数据类型

export interface loginResponeData{
    
    
    code:number,
    data:dataType
}

4.登录时间判断(细节)
//login.vue
//导入
import {
    
    getTime} from '../../util/time'

  $router.push('/')
   ElNotification({
    
    
     type:'success',
     message:'登录成功',
     title:`HI,${
      
      getTime()}好!`
   })
//创建util/time.ts
//获取时间
 export const getTime=()=>{
    
    
    let message=''

    const  hours=new Date().getHours()
    if(hours<=9){
    
    
        message='早上'
    }else if(hours<=12){
    
    
        message='上午'
    }else if(hours<=18){
    
    
        message='下午'
    }else{
    
    
        message='晚上'
    }

    return message
}
5.登录表单校验(细节)

根据element-plus中的Form的进行

Form 表单 | Element Plus (element-plus.org)

//ref和prop在表单效验的作用

ref 是用于获取 DOM 元素或组件实例的引用。通过 ref,你可以在 Vue 组件中获取到对应 DOM 元素或组件实例的引用,进而进行操作或访问其属性和方法。在表单验证中,常常使用 ref 来获取表单元素的引用,以便进行表单验证操作和其他操作,比如提交表单和重置表单。

prop 是用于设置验证规则的属性。在使用 Element Plus 或其他表单验证库时,你可以通过 prop 来为表单元素设置验证规则。验证规则可以包括必填、最小长度、最大长度、正则表达式等规则,用于验证用户的输入是否符合要求。通过设置 prop 属性,表单验证库会根据这些规则进行验证,并给出相应的错误提示或处理。

具体来说,在 Element Plus 中,使用 prop 属性可以为表单元素配置验证规则。这些验证规则作为一个对象数组传递给 rules 属性,每个对象包含了验证规则的具体内容,如 required: true 表示必填规则。通过设置 rules 属性,Element Plus 会在用户输入或表单提交等操作时,自动进行表单验证,并提供错误提示或处理。

综上所述,ref 主要用于获取 DOM 元素或组件实例的引用,以进行操作和访问;prop 则是定义了验证规则,用于进行表单验证。这两者在表单验证过程中具有不同的作用

//自定义的规则的好处
可以自行定义和判断特殊需求

//对比
//非自定义
desc: [
    { required: true, message: 'Please input activity form', trigger: 'blur' },
  ],
//自定义
 if(value===''){
   callback(new Error('密码不能为空!'))
 }else if(value.length>=5&&value.length<=15){
   callback()
 }else{
   callback(new Error("密码长度必须在5-15之间"))
 }
2.layout页面
1.静态页面搭建

弹性盒子布局:display:flex(弹性盒子布局)详解_display: flex_羡阳公子的博客-CSDN博客

1.logo组件静态搭建
<template>
  <div class="log">
       <img :src="setting.logo" >
       <p>{
   
   {setting.title}}</p>
  </div>
</template>

<script setup lang="ts">
import setting from '../../setting';
</script>
<style scoped lang="scss">

.log{
    height:$base-menu-logo-height;
    width: 100%;
    color: white;
    display: flex;
    align-items: center;
    img{
        width: 40px;
        height: 40px;
    }
    p{
        font-size: $base-logo-title-fontSize;
        margin-left:10px;
    }
}

</style>
2.菜单栏menu
//menu组件

<template>
  <div class="layout_container">
    <!--左侧菜单-->
    <div class="layout_slider">

      <Logo></Logo>
      <el-scrollbar class="scrollbar">
        <!--菜单组件-->
        <el-menu  background-color="#001529" text-color="white" >
          <!--父传值-->
         <Menu :menuList="userStore.menuRoutes"></Menu>
          <!---折叠的菜单-->
        </el-menu>
      </el-scrollbar>
    </div>

    <!--顶部菜单-->
    <div class="layout_tabbar"></div>
    <!--内容展示区域-->
    <div class="layout_main">
     <router-view></router-view>
    </div>
  </div>
</template>

<script setup lang="ts">
import Menu from './menu/index.vue';
import Logo from './logo/index.vue';


import useUserStore from '../store/modules/user';
const userStore=useUserStore()
</script>


<style scoped lang="scss">
.layout_container {
  width: 100%;
  height: 100vh;
  background-color: palegoldenrod;
  .layout_slider {
    width: $base-menu-width;
    height: 100vh;
    background-color: $base-menu-background;
    .scrollbar {
      width: 100%;
      height: calc(100vh - $base-menu-logo-height);
      .el-menu{
        border-right: none;
      }
    }
  }
  .layout_tabbar {
    position: fixed;
    width: calc(100% - $base-menu-width);
    height: $base-tabbar-hieght;
    background-color: cadetblue;
    top: 0px;

    left: $base-menu-width;
  }
  .layout_main {
    position: absolute;
    width: calc(100% - $base-menu-width);
    height: calc(100vh - $base-tabbar-hieght);
    background-color: red;
    left: $base-menu-width;
    top: $base-tabbar-hieght;
    padding: 20px;
    //滚动条
    overflow: auto;
  }
}
</style>
//小仓库user.ts
  state:():UserState=>{
    
    
        return{
    
    
            token: GET_TOKEN(),
            menuRoutes:constantRoute,//生成菜单需要的数组
        }
    }
  
  //小仓库type.ts
  export interface UserState{
    
    
    token:string|null;
    menuRoutes:RouteRecordRaw[]
}
//route.ts配置
//对外暴露配置路由
export const constantRoute=[
    {
    
    
        path:'/login',
        component:()=>import('@/views/login/index.vue'),
        name:'login',//路由命名
        meta:{
    
    
            title:'登录',
            hidden:true//路由在标题是否隐藏
        }
    },
    {
    
    
        path:'/',
        component:()=>import('@/layout/index.vue'),
        name:'layout',
        meta:{
    
    
            title:'layout',
            hidden:false,//路由在标题是否隐藏
            icon:'Avatar'
        },
        redirect:'/home',
        children:[
            {
    
    
                path:'/home',
                component:()=>import('@/views/home/index.vue'),
                meta:{
    
    
                    title:'首页',
                    hidden:false,//路由在标题是否隐藏
                    icon:'HomeFilled'
                }
            }
           
        ]
    },
    {
    
    
        path:'/404',
        component:()=>import('@/views/404/index.vue'),
        name:'404',
        meta:{
    
    
            title:'404',
            hidden:true//路由在标题是否隐藏
        }
    },
    {
    
    
        path:'/screen',
        component:()=>import('@/views/screen/index.vue'),
        name:'scrren',//路由命名
        meta:{
    
    
            title:'数据大屏',
            hidden:false,//路由在标题是否隐藏
            icon:'DataBoard'
        }
    },
    {
    
    
        path:'/acl',
        component:()=>import('@/layout/index.vue'),//注意这里
        name:'Acl',//路由命名
        meta:{
    
    
            title:'权限管理',
            hidden:false,//路由在标题是否隐藏
            icon:'Lock'
        },
        children:[
            {
    
    
                path:'/acl/permission',
                component:()=>import('@/views/acl/permission/index.vue'),
                name:'Permission',
                meta:{
    
    
                    title:'菜单管理',
                    hidden:false,//路由在标题是否隐藏
                    icon:'Monitor'
                }
            },
            {
    
    
                path:'/acl/role',
                component:()=>import('@/views/acl/role/index.vue'),
                name:'Role',
                meta:{
    
    
                    title:'角色管理',
                    hidden:false,//路由在标题是否隐藏
                    icon:'UserFilled'
                }
            },
            {
    
    
                path:'/acl/user',
                component:()=>import('@/views/acl/user/index.vue'),
                name:'User',
                meta:{
    
    
                    title:'用户管理',
                    hidden:false,//路由在标题是否隐藏
                    icon:'User'
                }
            }
           
        ]
    },
    {
    
    
        path:'/product',
        component:()=>import('@/layout/index.vue'),//注意这里
        name:'Product',//路由命名
        meta:{
    
    
            title:'商品管理',
            hidden:false,//路由在标题是否隐藏
            icon:'Goods'
        },
        children:[
            {
    
    
                path:'/product/attr',
                component:()=>import('@/views/product/attr/index.vue'),
                name:'Attr',
                meta:{
    
    
                    title:'属性管理',
                    hidden:false,//路由在标题是否隐藏
                    icon:'ChromeFilled'
                }
            },
            {
    
    
                path:'/product/sku',
                component:()=>import('@/views/product/sku/index.vue'),
                name:'Sku',
                meta:{
    
    
                    title:'SKU管理',
                    hidden:false,//路由在标题是否隐藏
                    icon:'Grid'
                }
            },
            {
    
    
                path:'/product/spu',
                component:()=>import('@/views/product/spu/index.vue'),
                name:'Spu',
                meta:{
    
    
                    title:'SPU管理',
                    hidden:false,//路由在标题是否隐藏
                    icon:'Calendar'
                }
            },
            {
    
    
                path:'/product/trademark',
                component:()=>import('@/views/product/trademark/index.vue'),
                name:'Trademark',
                meta:{
    
    
                    title:'品牌管理',
                    hidden:false,//路由在标题是否隐藏
                    icon:'ShoppingCartFull'
                }
            }
           
        ]
    },
    
    {
    
    
        path:'/:pathMatch(.*)*',
        redirect:'/404',
        name:'Any',
        meta:{
    
    
            title:'任意路由',
            hidden:true//路由在标题是否隐藏false表示隐藏
        },
        
    }
]
3.tabbar栏搭建
  • 组件祖传孙(重点)用小仓库
  • 折叠效果
//tabber.vue
<template>
  <div class="tabbar">
     <div class="tabbar_left">
    <Breadcrumb></Breadcrumb>
     </div>
    <div class="tabbar_right">
      
        <Setting></Setting>
    </div>
  </div>
</template>

<script setup lang="ts">
import Breadcrumb from './breadcrumb/index.vue';
import Setting from './setting/index.vue';
</script>
<script lang="ts">

 export default{
     // eslint-disable-next-line vue/no-reserved-component-names
     name:'Tabbar'
 }
</script>
<style scoped lang="scss">
.tabbar {
  height: 100%;
  width: 100%;
  display: flex;
  justify-content: space-between;
  background-image: linear-gradient(to right,white,rgb(44, 145, 161));

.tabbar_left{
      display: flex;
      align-items: center;
      margin-left: 10px;
  }
  
   .tabbar_right{
      display: flex;
      align-items: center;
     
  }
}
</style>


<template>
  <el-button size="small" icon="Refresh" circle @click="updateRefresh" ></el-button>
        <el-button  size="small" icon="FullScreen" circle @click="fullScreen"></el-button>
        <el-button  size="small" icon="Setting" circle ></el-button>
        <img src="../../../logo.png" style="height:24px;width:24px;margin:0 10px">
        <!--下拉菜单-->
     <el-dropdown>
    <span class="el-dropdown-link">
        admin
      <el-icon class="el-icon--right">
        <arrow-down />
      </el-icon>
    </span>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item>Action 1</el-dropdown-item>
        
 //setting.vue   
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup lang="ts">

import  useLayoutSettingStore from '../../../store/modules/setting';

const  layoutSettingStore= useLayoutSettingStore();
//点击刷新
const updateRefresh=()=>{
      layoutSettingStore.refresh=!layoutSettingStore.refresh;
}
//点击全屏
const fullScreen=()=>{
  //dom属性,//全屏为true
  const full=document.fullscreenElement
  if(!full){
    document.documentElement.requestFullscreen()
  }else{
    document.exitFullscreen();
  }
}

</script>
<script lang="ts">

 export default{
     // eslint-disable-next-line vue/no-reserved-component-names
     name:'Setting'
 }
</script>
<style scoped lang="scss">


</style>

//BreadCrumb.vue

<template>
      <!-- <div class="bread">-->
    <!--左则静态图标-->
    <el-icon style="margin-right: 10px" @click="changeIcon">
    <component :is="LayoutSettingStore.fold?'Fold':'Expand'"></component>
    </el-icon>
    <!--右则面包屑-->

    <el-breadcrumb separator-icon="ArrowRight" >
    <el-breadcrumb-item v-for="(item , index) in $router.matched" :key="index" v-show="item.meta.title" :to="item.path" >
    
    <el-icon>
    <component :is="item.meta.icon"></component>
    </el-icon>
     <span>{
   
   {item.meta.title}}</span>
     </el-breadcrumb-item>
    </el-breadcrumb>
  

</template>

<script setup lang="ts">

import useLayoutSettingStore from '../../../store/modules/setting';
const LayoutSettingStore=useLayoutSettingStore()

import { useRoute} from 'vue-router'

const $router=useRoute()
console.log($router.matched[0].meta)

const changeIcon=()=>{
  LayoutSettingStore.fold=!LayoutSettingStore.fold
}
</script>
<script lang="ts">

 export default{
     // eslint-disable-next-line vue/no-reserved-component-names
     name:'BreadCrumb'
 }
</script>

<style scoped lang="scss">
/*.bread{
      display: flex;
      align-items: center;
      margin-left: 10px;
  }*/
  
</style>
//小仓库:setting.ts

import {
    
    defineStore} from 'pinia';

const useLayoutSettingStore=defineStore('SettingStore',{
    
    
    state:()=>{
    
    
        return {
    
    
            fold:false,//控制用户折叠,
            refresh:false,//控制刷新效果
        }
    }
}) 

export default useLayoutSettingStore;
4.获取用户信息和token的理解
  • 理解token在请求拦截器的原因
//store/module/user.ts
async userInfo(){
    
    
            //获取用户信息中的头像和名字
            const result=await reqUserInfo()
           if(result.code===200){
    
    
               this.username=result.data.checkUser.username,
               this.avatar=result.data.checkUser.avatar
           }
        },
              
  //home.vue
  import useUserStore from '../../store/modules/user'

const useStore=useUserStore();

onMounted(()=>{
    
    
  useStore.userInfo()
})
//在util/request.ts
request.interceptors.request.use((config)=>{
    
    

    const UserStore= userUserStore()
    if(UserStore.token){
    
    
        config.headers.token=UserStore.token;
    }

    return config
})

这样子userInfo就能获取到信息了

5.退出登录
  • 退出首页
  • 删除token
  • 发出退出接口请求
  • 理解useRoute和useRouter的区别
//store/module/user.ts
     userLogout(){
    
    
            this.token='',
            this.username='',
            this.avatar='',
            REMOVE_TOKEN()
        }
        
//在tabbar/setting.vue 
import {
    
    useRouter,useRoute} from 'vue-router';
  //退出登录
const logout=()=>{
    
    

  userStore.userLogout();
  $router.push({
    
    path:'/login',query:{
    
    redirect:$route.path}})
}   

//login/index.vue
 //跳转
   const redirect :any=$route.query.redirect
   $router.push({
    
    path:redirect ||'/'})

抛出问题:

  • 全局守卫解决

  • 刷新userinfo的信息消失解决(在守卫获取名字信息)为什么不持久化

6.路由鉴权业务(解决全局守卫)

//permission.ts
//路由鉴权
import router from '@/router';
import nprogress from 'nprogress';
import setting from './setting';
import 'nprogress/nprogress.css';
nprogress.configure({
    
    showSpinner:false})
import userUserStore from '@/store/modules/user'

import pinia from '@/store';
const userStore  = userUserStore(pinia)
//全局前置守卫
router.beforeEach( async (to:any,from:any,next:any)=>{
    
    
        document.title=setting.title+'-'+to.meta.title
    nprogress.start()

  
    const token=userStore.token;
    const username=userStore.username;
   
    if (token) {
    
    
        // 用户已登录
        if(to.path==='/login'){
    
    
            next({
    
    path:'/'})
        }else{
    
    
            //其余的六个路由
            //有用户信息
            if(username){
    
    
                next()
            }
            else{
    
    
                //发请求获取用户信息才放行
                try{
    
    

                await  userStore.userInfo()
                next()
                }catch(error){
    
    
                    //token过期或手动修改了本地
                    userStore.userLogout()
                    next({
    
    path:'/login',query:{
    
    redirect:to.path}})
                }

            }
           
        }
       
    } else {
    
    
        // 用户未登录
        if (to.path === '/login') {
    
    
            next();
        } else {
    
    
            // 重定向到登录页
            next({
    
    path:'/login',query:{
    
    redirect:to.path}});
        }
    }
})


//全局后置守卫
router.afterEach((to:any,from:any)=>{
    
    

    nprogress.done()

})

//第一个问题:任意路由切换实现进度业务  ---nprogress
//第二个问题:路由鉴权
//用户未登录之前:只能登录/login
//用户登录成功之后:不能登录/login




//main.ts引入
import './permission'
即可使用
6.代理跨域
//vite.config.ts
import {
    
     defineConfig,loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import {
    
     createSvgIconsPlugin } from 'vite-plugin-svg-icons' // 修改此处
import {
    
     viteMockServe } from 'vite-plugin-mock';

// https://vitejs.dev/config/
export default defineConfig(({
    
    command,mode})=>{
    
    
  //获取各个环境下对应的变量
  const env=loadEnv(mode,process.cwd())
  //mode默认开发
  return {
    
    
    //代理跨域
    server:{
    
    
      proxy:{
    
    
        [env.VITE_APP_BASE_API]:{
    
    
          target:env.VITE_SERVE,
          changeOrigin:true,
          rewrite:(path)=>path.replace(/^\/api/,'')
        }
      }
    }
  }
})


3.品牌管理搭建
1.静态页面搭建

<template>
  <div>
    <el-card class="box-card">
      <el-button size="default" type="primary" icon="Plus">添加品牌</el-button>

      <!--表格-->
      <el-table  style="margin:10px 0" border :data="trademarkArr" >
        <el-table-column label="序号" type="index" width="80px" align="center"></el-table-column>
        <el-table-column label="品牌名称"  >
          <template #default="{row,$index}">
          <pre>{
   
   {row.tmName}}</pre>
          </template>
        </el-table-column>
        <el-table-column label="品牌LOGO">
             <template #default="{row,$index}">
          <img  :src="row.logoUrl" style="height:100px;width:100px"/>
          </template>
        </el-table-column>
        <el-table-column label="品牌操作">
             <template #default="{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>
    </el-card>


    <!--分页器-->
   
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
   
      :current-page="pageNo"
      :page-sizes="[3, 5, 7, 9]"
      :page-size="limit"
      layout="prev, pager, next, jumper,->,sizes,total"
      :total="total">
    </el-pagination>
 
  </div>
</template>

<script setup lang="ts">
import {onMounted, ref} from 'vue';
import {reqHasTrademark} from '../../../api/product/trademark/index';
import type { Records, TradeMarkResponseData } from '../../../api/product/trademark/type';
//当前页码
const pageNo=ref<number>(1)
//每页展示多少条数据
const limit=ref<number>(3)
const total=ref<number>(0)
const trademarkArr=ref<Records>([])
//获取请求的方法
const getHasTrademark= async()=>{
  let result:TradeMarkResponseData=await reqHasTrademark(pageNo.value,limit.value)
  console.log(result)
  if(result.code===200){
      total.value=result.data.total
      trademarkArr.value=result.data.records

  }

}
const hanleCurrentChange=()=>{
  
}

onMounted(()=>{
  getHasTrademark()
})
</script>
<style scoped lang="scss">
</style>

2.分页pagination处理的业务步骤
  • 当前页面的变化current-change就发请求展示数据
  • 当选择下拉菜单变化时size-change,页码应该为1,应该在请求数据方法默认为1

  • current-page和page-size需要实时响应接收所有用v-model

    <el-pagination
      @size-change="handleSizePage"
      @current-change="getHasTrademark"
      v-model:current-page="pageNo"
      :page-sizes="[3, 5, 7, 9]"
      v-model:page-size="limit"
      layout="prev, pager, next, jumper,->,sizes,total"
      :total="total">
    </el-pagination>
    
    
    //当前页码
const pageNo=ref<number>(2)
//每页展示多少条数据
const limit=ref<number>(3)
const total=ref<number>(0)
const trademarkArr=ref<Records>([])
//获取请求的方法
const getHasTrademark= async(pager=1)=>{
  pageNo.value=pager
  let result:TradeMarkResponseData=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 handleSizePage= ()=>{
  getHasTrademark()
}
3.dialog对话框
1.静态页面
 <!--对话框-->
  <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>
2.添加和编辑品牌的处理
  • 编辑需要ID,添加不需要ID
  • 用同一个req需要判断id而选择api
export const reqAddORUpdateTrademark=(data:TradeMark)=>{
    
    
    if(data.id){
    
    
        return request.put<any,any>(API.UPDATETRADEMARK_URL,data)
    }else{
    
    
        return request.post<any,any>(API.ADDTRADEMAKR_URL,data)
    }
}

1.添加的业务处理

  • 展示对话框并清空数据
  • 收集数据并发出请求
//添加品牌收集的数据
const trademarkParams=reactive<TradeMark>({
  tmName:'',
  logoUrl:''
})

//添加品牌
//问题一:为什么在这里清空
//为了减少多次使用
const addTrademark=()=>{
  dialogFormVisible.value=true
  //清空数据
  trademarkParams.tmName=''
  trademarkParams.logoUrl=''
  trademarkParams.id=0//不然修改之后没办法清空要加这个
}

//这里只是规范而言

//触发文件上传之前的钩子---->控制文件的格式不然默认什么文件都可以
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
  //文件要求:png||gif||jpg 4mb

  if (rawFile.type == 'image/jpeg' || rawFile.type =='image/png' || rawFile.type ==='image/gif') {
    if (rawFile.size / 1024 / 1024 < 4) {
        return true
    }else{
         ElMessage.error('Avatar picture size can not exceed 4MB!')
         return false
    }
   
  
  } else  {
     ElMessage.error('Avatar picture must be JPG or GIF or PNG format!')
    return false
  }
}
//文件上传成功的钩子
const handleAvatarSuccess: UploadProps['onSuccess'] = (response,uploadFile) => {
  //respon当前这次上传图片post请求服务器返回的数据
  trademarkParams.logoUrl=response.data
  console.log(response.data)
}
3.修改业务的处理
  • row里面有id的数据
  • 然后把存在的数据放在对话框展示出来
  • 修改完毕,然后发请求
//编辑
const updateTrademark= async(row:TradeMark)=>{
  dialogFormVisible.value=true
  //可以使用合并语法object.assign(trademarkParams,row)
  trademarkParams.id=row.id
  trademarkParams.tmName=row.tmName
  trademarkParams.logoUrl=row.logoUrl
}

4.修改和添加共同使用的部分
  • 对话框点击确认后需要处理他们的不同信息提示
<el-dialog v-model="dialogFormVisible" :title="trademarkParams.id?'修改品牌':'添加品牌'">

//对话框取消
const cancel=()=>{
  dialogFormVisible.value=false
}
//对话框确定
const confirm= async()=>{
   const result= await reqAddORUpdateTrademark(trademarkParams)
   if(result.code===200){
       
  dialogFormVisible.value=false
     ElMessage({
       type:'success',
       message:trademarkParams.id?'修改品牌成功':'添加品牌失败'
     })
     //再发请求获取所有的数据
     getHasTrademark(trademarkParams?pageNo.value:1)
   }else{
       ElMessage({
       type:'error',
       message:trademarkParams.id?'修改品牌失败':'添加品牌失败'
     })
       dialogFormVisible.value=false
   }

}
5.表单校验
  • 表单效验中的:model和v-model不是一个意思,看element-plus,一定要加ref和prop不然不生效
  • 考虑清除的情况,点击编辑和修改时要清除数据
const  formRef= ref<FormInstance>()
//添加品牌
const addTrademark=()=>{
    
    


  dialogFormVisible.value=true
  //清空数据
  trademarkParams.tmName=''
  trademarkParams.logoUrl=''
  trademarkParams.id=0

  //清除数据第一种写法
   // formRef.value?.clearValidate('logoUrl')
    //formRef.value?.clearValidate('tmName')
//第二种写法
nextTick(()=>{
    
    
  formRef.value.clearValidate('logoUrl')
    formRef.value.clearValidate('tmName')
})

}
//编辑
const updateTrademark= async(row:TradeMark)=>{
    
    
  dialogFormVisible.value=true
  //可以使用合并语法object.assign(trademarkParams,row)
  trademarkParams.id=row.id
  trademarkParams.tmName=row.tmName
  trademarkParams.logoUrl=row.logoUrl
  //第二种写法
nextTick(()=>{
    
    
  formRef.value.clearValidate('logoUrl')
    formRef.value.clearValidate('tmName')
})
  
}

//文件上传成功的钩子
const handleAvatarSuccess: UploadProps['onSuccess'] = (response,uploadFile) => {
    
    
  //respon当前这次上传图片post请求服务器返回的数据
  trademarkParams.logoUrl=response.data
  console.log(response.data)
  formRef.value.clearValidate('logoUrl')
}

//品牌自定义规则

const validatorTmName = (rule: any, value: any, callback: any) => {
    
    
  console.log(value)
   if(value.trim().length>=2){
    
    
     callback()
   }else{
    
    
     callback(new Error('品牌名称位数大于等于两位'))
   }
}

const validatorLogoUrl= (rule: any, value: any, callback: any) => {
    
    
  if(value){
    
    
    callback()
  }else{
    
    
    callback(new Error('LOGO图片务必上传'))
  }
 
}



//表单校验
const rules={
    
    
  //required是出来的五角星 
  tmName:[  {
    
     required:true,trigger:'blur',validator:validatorTmName   } ],
  logoUrl:[   {
    
      required:true,validator:validatorLogoUrl}    ]
}
4.删除业务
  • 注意删除之后分页器怎么跳转到第几页的判断
  <!--气泡框-->
     
  <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>



//气泡确认的回调
const removeTradeMark= async(id:number)=>{
    
    

const result= await reqDeleteTrademark(id)
console.log(result)
  if(result.code==200){
    
    
     ElMessage({
    
    
       type:'success',
       message:'品牌删除成功'
     })
     //再发请求获取所有的数据
     getHasTrademark(trademarkArr.value.length>1?pageNo.value:pageNo.value-1)
   }else{
    
    
       ElMessage({
    
    
       type:'error',
       message:'品牌删除失败'
     })
     
   }

}
4.属性管理搭建
1.静态页面搭建

<template>
  <div>
    <Category></Category>
      <el-card  style="margin-top:15px" >
      <el-button type="primary" size="default" icon="Plus">添加属性</el-button>
      <el-table  border style="margin:10px 10px">
      <el-table-column type="index" width="80" align="center" label="序号" ></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">

</script>
<style scoped lang="scss">
</style>



<!--组件Category的页面-->
 <el-card >
        <el-form :inline="true">
          <el-form-item label="一级分类" >
          <el-select v-model="categoryStore.c1Id" @change="handler1">
          <!--option: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 v-model="categoryStore.c2Id"  @change="handler2">
          <!--option:label即为显示文字,value属性即为select下拉菜单的数据-->
            <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 v-model="categoryStore.c3Id">
          <!--option:label即为显示文字,value属性即为select下拉菜单的数据-->
            <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>
2.三级分类的业务
  • 由于需要父传子传信息,所以目前的策略是用仓库存储数据,全局使用
  • 需要拿到他们的id,@change的意义
  • 清空数据的处理
  • 禁用添加品牌禁用,用id3
<!--组件Category的页面-->
<template>

    <el-card >
        <el-form :inline="true">
          <el-form-item label="一级分类" >
          <el-select v-model="categoryStore.c1Id" @change="handler1">
          <!--option: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 v-model="categoryStore.c2Id"  @change="handler2">
          <!--option:label即为显示文字,value属性即为select下拉菜单的数据-->
            <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 v-model="categoryStore.c3Id">
          <!--option:label即为显示文字,value属性即为select下拉菜单的数据-->
            <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';
const categoryStore=useCategoryStore()

//组件挂载完毕先获取一级分类的数据
onMounted(()=>{

categoryStore.getC1()
})
//选中一级分类的值保证ID有了就会触发二级分类
const handler1=()=>{
    //一级点击时先清空
    categoryStore.c2Id=''
     categoryStore.c3Id=''
      categoryStore.c3Arr=[]
    categoryStore.getC2()
  
}
//选中二级分类的值保证ID有了就会触发二级分类
const handler2=()=>{
      //二级点击时先清空3级id
     categoryStore.c3Id=''  
    categoryStore.getC3()
}
</script>
<style scoped>

</style>
//小仓库:layout组件相关配置
import {
    
    defineStore} from 'pinia';
import {
    
    reqC1, reqC2, reqC3} from '@/api/product/attr';
import {
    
     CategoryResponseData } from '@/api/product/attr/type';
import {
    
    CategoryState} from './types/type';

const useCategoryStore=defineStore('Category',{
    
    
    state:():CategoryState =>{
    
    
        return {
    
    
            //一级分类数据
        c1Arr:[],
        //c1ID
        c1Id:'',
        //c2ID
        c2Id:'',
        //c3Id
        c3Id:'',
        //二级分类数据
        c2Arr:[],
        //三级分类数据
        c3Arr:[]
        }
    },
    actions:{
    
    
      async getC1(){
    
    
            const result: CategoryResponseData= await reqC1()
            if(result.code===200){
    
    
                 this.c1Arr=result.data
            }
    
        } ,

        async getC2(){
    
    
            const result:CategoryResponseData=await reqC2(this.c1Id)
            console.log(result)
            if(result.code===200){
    
    
                this.c2Arr=result.data
            }
            
        },
        async getC3(){
    
    
            const result:CategoryResponseData=await reqC3(this.c2Id)
            if(result.code===200){
    
    
                this.c3Arr=result.data
            }          
        }
    },
    getters:{
    
    

    }
}) 
export default useCategoryStore;

//api
import request from '@/util/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=(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)
3.属性管理业务处理
1.展示数据业务
  • 当三级ID有变化则请求数据并展示出来,否则不请求,并用watch监听,监听一开始需要清空
<template>
  <div>
    <Category></Category>
      <el-card  style="margin:15px 0" >
      <el-button type="primary" size="default" icon="Plus" :disabled="categoryStore.c3Id?false:true">添加属性</el-button>
      <el-table  border style="margin:10px 10px" :data="attrArr">
      <el-table-column type="index" width="80" align="center" label="序号" ></el-table-column>
      <el-table-column label="属性名称"  width="120px" prop="attrName" ></el-table-column>
       <el-table-column label="属性值名称"  >
         <template #default="{row,$index}">
  <el-tag v-for="(item,index) in row.attrValueList" :key="item.id">{
   
   {item.valueName}}</el-tag>

</template>
       </el-table-column>
      <el-table-column label="操作"  width="120px"  >
             <template #default="{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>
          
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { watch,ref } from 'vue';
import useCategoryStore from '../../../store/modules/category';
import{reqAttr} from '../../../api/product/attr/index';
import { AttrResponseData,Attr } from '../../../api/product/attr/type';
const categoryStore=useCategoryStore()
//存贮数据
const attrArr=ref<Attr[]>([])

//监听三级id的变化请求数据

watch(()=>categoryStore.c3Id,()=>{
 
    //保证有三级id
   //如果三级id无变化时,不请求且页面清空
 attrArr.value=[]
    if(!categoryStore.c3Id) return 
      getAttr()
 
})

const getAttr=async()=>{
  //获取分类的ID
const {c1Id,c2Id,c3Id}=categoryStore
  const result:AttrResponseData= await reqAttr(c1Id,c2Id,c3Id)
  if(result.code===200){
    attrArr.value=result.data
  }

}

</script>
<style scoped lang="scss">
</style>
2.添加业务处理
  • 点击添加需要更换添加属性值页面并且三级分类禁用判断需要父传子props
  • 写接口、处理收集的数据、发请求、写基础功能
  • 细化功能

<template>
  <div>
    <Category :scene="scene"></Category>
      <el-card  style="margin:15px 0" >
     <div v-show="scene==0">
    <el-button type="primary" size="default" icon="Plus" :disabled="categoryStore.c3Id?false:true" @click="addAttr">添加属性</el-button>
      <el-table  border style="margin:10px 10px" :data="attrArr">
      <el-table-column type="index" width="80" align="center" label="序号" ></el-table-column>
      <el-table-column label="属性名称"  width="120px" prop="attrName" ></el-table-column>
       <el-table-column label="属性值名称"  >
         <template #default="{row,$index}">
            <el-tag v-for="(item,index) in row.attrValueList" :key="item.id">{
   
   {item.valueName}}</el-tag>
        </template>
       </el-table-column>
      <el-table-column label="操作"  width="120px"  >
             <template #default="{row,$index}">
          <el-button type="primary" size="small" icon="Edit" @click="updateAttr(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>
     <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  icon="Plus" type="primary" :disabled="attrParams.attrName?false:true"  @click="addAttrValue">添加属性值</el-button>
         <el-button type="primary" @click="cancel">取消</el-button>
      <el-table border style="margin:10px 0" :data="attrParams.attrValueList">

        <el-table-column width="80px" type="index" align="center" label="序号"></el-table-column>
          <el-table-column label="属性值名称">
           <template #default="{row,$index}">
           <el-input v-if="row.flag" @blur="toBlur(row,$index)" placeholder="请输入属性值名称"  v-model="row.valueName"></el-input>
           <div v-else @click="toEdit(row)">{
   
   {row.valueName}}</div>
        </template>
          </el-table-column>
            <el-table-column label="属性值操作">
                  <template  #default="{row,$index}">
          <el-button type="primary" size="small" icon="Delete" ></el-button>
         </template>
            </el-table-column>
            
      </el-table>
        <el-button type="primary" @click="save" :disabled="attrParams.attrValueList.length>0?false:true">保存</el-button>
         <el-button type="primary"  @click="cancel">取消</el-button>

     </div>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { watch,ref, reactive,nextTick } from 'vue';
import useCategoryStore from '../../../store/modules/category';
import{reqAttr, reqUpdateOrAddAttr} from '../../../api/product/attr/index';
import { AttrResponseData,Attr } from '../../../api/product/attr/type';
import { ElMessage } from 'element-plus'
const categoryStore=useCategoryStore()
//存贮数据
const attrArr=ref<Attr[]>([])
//切换内容
const scene=ref<number>(0)

//收集新增属性的数据

const attrParams=reactive<Attr>({
attrName: "",
  attrValueList: [],
    categoryId: 0,
  categoryLevel: 3,//三级分类
})
//监听三级id的变化请求数据

watch(()=>categoryStore.c3Id,()=>{
 
    //保证有三级id
   //如果三级id无变化时,不请求且页面清空
 attrArr.value=[]
    if(!categoryStore.c3Id) return 
      getAttr()
 
})



const getAttr=async()=>{
  //获取分类的ID
const {c1Id,c2Id,c3Id}=categoryStore
  const result:AttrResponseData= await reqAttr(c1Id,c2Id,c3Id)
 
  if(result.code===200){
    attrArr.value=result.data
  }

}

//添加属性
const addAttr=()=>{
  //清空
  Object.assign(attrParams,{
attrName: "",
  attrValueList: [],
    categoryId: 0,
  categoryLevel: 3,//三级分类
})
  scene.value=1
  attrParams.categoryId=categoryStore.c3Id
}

//修改属性
const updateAttr=(row)=>{
  scene.value=1
  console.log(row)
}

//取消
const cancel=()=>{
  scene.value=0
}
//添加属性值
const addAttrValue=()=>{
  attrParams.attrValueList.push({
    valueName:'',
    flag:true
  })
}

//点击保存
const save= async()=>{
//参数收集完毕发请求
 const result=await reqUpdateOrAddAttr(attrParams)

if(result.code===200){
    scene.value=0
    ElMessage({
       type:'success',
       message:attrParams.id?'修改成功':'添加成功'
     })
     //重新刷新
     getAttr()

}else{
   ElMessage({
       type:'error',
       message:attrParams.id?'修改失败':'添加失败'
     })
}
}
//失去焦点
const toBlur=(row,$index)=>{
  
  //非法情况一
    if(row.valueName.trim()==''){
      //删除不让占位
      attrParams.attrValueList.splice($index,1)
      
    ElMessage({
       type:'error',
       message:'属性值不能为空'
     })
  }
  //非法情况二属性值不能相同
 const repeat= attrParams.attrValueList.find((item)=>{

    if(item!=row){
      return item.valueName===row.valueName
    }
  })
  if(repeat){
       attrParams.attrValueList.splice($index,1)
     ElMessage({
       type:'error',
       message:'属性值不能重复'
     })
  }

  row.flag=false
}
//获取点击
const toEdit=(row)=>{
  row.flag=true
  
}
</script>
<style scoped lang="scss">
</style>

3.表单聚焦业务
  • 点击立马聚焦
  • 自己有点难理解:ref=“(vc)=>inputArr=vc”

   <el-input :ref="(vc)=>inputArr[$index]=vc" v-if="row.flag" @blur="toBlur(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  #default="{row,$index}">
          <el-button type="primary" size="small" icon="Delete" @click="deleteParams(row,$index)"></el-button>
         </template>
            </el-table-column>


<srcipt>
//添加属性值
const addAttrValue=()=>{
  attrParams.attrValueList.push({
    valueName:'',
    flag:true
  })
  //点击添加立马获取聚焦
    nextTick(()=>{
    inputArr.value[attrParams.attrValueList.length-1].focus()
  })
}
//获取点击
const toEdit=(row,$index)=>{
  row.flag=true
  //nextTick:响应式数据发生变化,获取更新的dom元素
  nextTick(()=>{
    inputArr.value[$index].focus()
  })
  
}
//删除属性值
const deleteParams=(row,$index)=>{
  attrParams.attrValueList.splice($index,1)
}
4.修改业务处理
  • 在添加时已绑定v-model,save方法根据ID去请求
  • 有坑:浅拷贝和深拷贝的问题
//修改属性
const updateAttr=(row)=>{
  scene.value=1
  //赋值给数据组,点击就会显示在页面上
  //深拷贝JSON或lodash的_.cloneDeep()
  //浅拷贝类似操作同一个对象(或地址)
Object.assign(attrParams,JSON.parse(JSON.stringify(row)));
}

5.删除属性和路由跳转清空的处理

//删除属性
const removeTradeMark=async(attrId:number)=>{

  const result =await reqDeleteAttr(attrId)
  if(result.code===200){
     ElMessage({
       type:'success',
       message:'删除成功'
     })
     getAttr()
  }else{
      ElMessage({
       type:'error',
       message:'删除失败'
     })
  }
  
}

onBeforeMount(()=>{
  //清空仓库分类数据
  categoryStore.$reset()
})
5.SPU管理模块
1.自己封装的组件pagination
//(使用了父子组件通信prop,自定义,v-model)
<template>
     <!--分页器-->
 
    <el-pagination
      @size-change="handleSizePage"
      @current-change="getprop.getReq"
      v-model:current-page="getprop.pageNo"
      :page-sizes="[3, 5, 7, 9]"
      v-model:page-size="getprop.limit"
      layout="prev, pager, next, jumper,->,sizes,total"
      :total="getprop.total">
    </el-pagination>

</template>
<script setup lang="ts">

//接收父组件传递过来的参数

 const getprop=defineProps({
     pageNo: {
      type: Number,
      required: true
    },
    limit: {
      type: Number,
      required: true
    },
    getReq: {
      type: Function,
      required: true
    },
    total:{
    type: Number,
      required: true
    }
})
// 当分页大小变化时的处理函数

const emit = defineEmits(["update:limit", "update:pageNo"]);

// 当分页大小变化时的处理函数
const handleSizePage = (newSize) => {
  emit("update:limit", newSize);
  getprop.getReq();
};

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

2.静态页面搭建

<template>
  <div>
    <Category :scene="scene"></Category>
    <el-card style="margin: 15px 0">
    <div v-show="scene===0">
       <el-button size="default" type="primary" icon="Plus" :disabled="categoryStore.c3Id?false:true"  @click="addSpu">添加SPU</el-button>
      <el-table border style="width: 100%" :data="records">
        <el-table-column label="序号" type="index" width="80px"></el-table-column>
        <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="操作" >
          <template #default="{row,$index}">
           <el-button size="small" type="primary" icon="Plus" title="添加SKU" @click="addSku"></el-button>
           <el-button size="small" type="primary" icon="Edit" title="修改SPU" @click="updateSpu(row.id)"></el-button>
            <el-button size="small" type="primary" icon="View" title="查看SKU列表"></el-button>
             <el-button size="small" type="primary" icon="Delete" title="删除SPU"></el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
      <!--添加SPU或修改-->
      <SpuForm  ref="spu"  @changeScene="changeScene"  v-show="scene===1" ></SpuForm>
      <!--添加sku-->
      <SkuForm v-show="scene===2"></SkuForm>
    </el-card>
     <!--一定要传3个参数
    pageNo:当前页
    limit:每页有多少个项目
    total:总共多少页,
    第4个根据页面的不同需求传的
      getReq:获取的数据请求,
    -->
    <Pagination v-model:pageNo="pageNo"  v-model:limit="limit"  :getReq="getHasSpu" :total="total"></Pagination>
  </div>
</template>

<script setup lang="ts">
import useCategoryStore from '../.../../../../store/modules/category';
import { reqHasSpu } from '../../../api/product/spu'
import {onBeforeMount, ref, watch} from 'vue';
import { Records } from '../../../api/product/spu/type';
import SkuForm from './skuForm.vue'
import SpuForm from './spuForm.vue';
const categoryStore=useCategoryStore()

const scene=ref<number>(0)
//当前页
const pageNo=ref<number>(1)
//页面个数
const limit=ref<number>(3)
//总数
const total=ref<number>(0)
//存储的SPU数据
const records=ref<Records>([])
//获取子组件得实例
const spu=ref<any>()
//获取三级ID或马上发请求
watch(()=>categoryStore.c3Id,()=>{

    if(!categoryStore.c3Id) return 
    getHasSpu()
})


const getHasSpu= async( pager=1)=>{
  pageNo.value=pager
  const result=await reqHasSpu(pageNo.value,limit.value,categoryStore.c3Id)
  if(result.code===200){
    total.value=result.data.total
    records.value=result.data.records
  }
}
//获取三级分类的数据
//添加spu
const addSpu=()=>{
  scene.value=1
}

//修改spu
const updateSpu=(row)=>{
  scene.value=1
  
}

//添加sku
const addSku=()=>{
  scene.value=2
}

//子组件通知父组件切换场景
const changeScene=(number:number)=>{
     scene.value=number
}
onBeforeMount(()=>{
  //清空仓库分类数据
  categoryStore.$reset()
})
</script>
<style scoped lang="scss">
</style>

3.修改SPU模块业务
  • 使用ref和defineExpose实现父子通信
  • 使用computed,filter(),every()
  • 有点乱有点多自己深刻理解一下

<template>
  <el-form label-width="100px" style="margin:15px 0">
    <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 v-model="SpuParams.tmId">
      <el-option  v-for="(item,index) 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 placeholder="请你输入SPU描述" type="textarea"    v-model="SpuParams.description"></el-input>
    </el-form-item>
      <el-form-item label="SPU图标" >
     <el-upload
     v-model:file-list="imgList"
    action="/api/ admin/product/fileUpload"
    list-type="picture-card"
    :on-preview="handlePictureCardPreview"
    :on-remove="handleRemove"
    :before-upload="beforeAvatarUpload"
  >
    <el-icon><Plus /></el-icon>
      <el-dialog v-model="dialogVisible">
    <img w-full :src="dialogImageUrl" alt="Preview Image"  style="width:100%;height:100%"/>
  </el-dialog>
  </el-upload>
    </el-form-item >
         <el-form-item label="SPU销售属性"  >
    <el-select :placeholder="unSelectSaleAttr.length?`还未选择${unSelectSaleAttr.length}个`:'无'"  v-model="saleAttrIdAndValueName">
      <el-option v-for="(item,index) in  unSelectSaleAttr" :key="item.id" :value="`${item.id}:${item.name}`" :label="item.name"></el-option>
      
    </el-select>
    <el-button style="margin:0 10px" type="primary" size="default" icon="Plus" @click="addSaleAttr" :disabled="saleAttrIdAndValueName?false:true">添加销售属性</el-button>
    <el-table :data="saleAttr">
      <el-table-column type="index" label="序号" width="80px" align="center"></el-table-column>
  
      <el-table-column  label="属性名" width="100px"  prop="saleAttrName"></el-table-column>
  
      <el-table-column label="属性值" >
        <template #default="{row,$index}">
         <el-tag  style="margin:0 5px" @close="row.spuSaleAttrValueList.splice(index,1)"   closable type="primary" v-for="(item,index) in row.spuSaleAttrValueList" :key="item.id" >{
   
   {item.saleAttrValueName}}</el-tag>
          <el-input style="width:150px" size="samll" v-model="row.saleAttrValue" @blur="toBlur(row)" placeholder="请你输入属性值"  v-if="row.flag==true" ></el-input>
          <el-button type="primary"  size="small" icon="Plus"  @click="addSaleAttrValue(row)" v-else ></el-button> 
         
          
        </template>
      </el-table-column>
       <el-table-column label="操作" width="100px">
        <template #default="{row,$index}">
          <el-button type="primary"  size="small" icon="Delete" @click="DeleteSaleAtrrValue(row,$index)"></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="small" @click="save">保存</el-button>
       <el-button  type="primary" size="small" @click="cancel">取消</el-button>
    </el-form-item>
    
  </el-form>
</template>

<script setup lang="ts">
import { AllTrademark, HasSaleAttr, HasSaleAttrRespnseData, SaleAttr, SaleAttrResponseData, SaleAttrValue, SpuData, SpuHasImg, SpuImg, Trademark } from "../../../api/product/spu/type"
import { reqAddOrUpdateSpu, reqAllSaleAttr, reqAllTrademark, reqSpuHasSaleAttr, reqspuImageList } from '../../../api/product/spu';
import {computed, ref} from 'vue';
import type { UploadProps} from 'element-plus'
import { ElMessage } from 'element-plus'

const $emit=defineEmits(['changeScene'])
//存储已有的spu数据
//存储全部品牌的数据
const allTradeMark=ref<Trademark[]>([])
  //存储商品图片
const imgList=ref<SpuImg[]>([])
  //销售属性
const saleAttr=ref<SaleAttr[]>([])
  //整个销售属性
const allSaleAttr=ref<HasSaleAttr[]>([])
//存储的SPU对象
const SpuParams=ref<SpuData>({
  category3Id: "",
  description: "",
    spuName: "",
     tmId: 0,
  //"id": 0,
  spuImageList: [],
  spuSaleAttrList: [],
 
})

//收集还未选择的销售属性
const saleAttrIdAndValueName=ref<string>('')
//点击取消
const cancel=()=>{
  $emit('changeScene',0)
}

//子组件书写方法

const initHasSpuData= async(row:SpuData)=>{
  //row传的数据不完整
  //存储已有的SPU
  SpuParams.value=row
  //获取品牌类型
 const result:AllTrademark=await reqAllTrademark()
 //获取商品图片的数据
 const result1:SpuHasImg=await reqspuImageList(row.id as number)

 //获取销售属性的数据
 const result2:SaleAttrResponseData=await reqSpuHasSaleAttr(row.id as number)
 //获取整个销售属性
 const result3:HasSaleAttrRespnseData=await reqAllSaleAttr()


 

//存储全部品牌的数据
  allTradeMark.value=result.data
  //存储商品图片
  //需要处理一下
  imgList.value=result1.data.map(item=>{
    return {
      name:item.imgName,
      url:item.imgUrl
    }
  })
  //销售属性
  saleAttr.value=result2.data
  //整个销售属性
  allSaleAttr.value=result3.data
  

}


const dialogImageUrl = ref('')
const dialogVisible = ref(false)
//上传图片的删除
const handleRemove: UploadProps['onRemove'] = (uploadFile, uploadFiles) => {
  console.log(uploadFile, uploadFiles)
}

//上传图片的预览
const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
  dialogImageUrl.value = uploadFile.url!
  dialogVisible.value = true
}

//触发文件上传之前的钩子---->控制文件的格式不然默认什么文件都可以
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
  //文件要求:png||gif||jpg 4mb

  if (rawFile.type == 'image/jpeg' || rawFile.type =='image/png' || rawFile.type ==='image/gif') {
    if (rawFile.size / 1024 / 1024 < 4) {
        return true
    }else{
         ElMessage.error('Avatar picture size can not exceed 4MB!')
         return false
    }
  
  } else  {
     ElMessage.error('Avatar picture must be JPG or GIF or PNG format!')
    return false
  }
}


//添加销售属性
const addSaleAttr=()=>{
  
 const [baseSaleAttrId,saleAttrName]=saleAttrIdAndValueName.value.split(':')

 const newSaleAttr:SaleAttr={
   baseSaleAttrId,
   saleAttrName,
   spuSaleAttrValueList:[]
 }

   saleAttr.value.push(newSaleAttr)
   //要清空
   saleAttrIdAndValueName.value=''
}
//添加销售属性中的属性值的tag和属性名
const addSaleAttrValue=(row:SaleAttr)=>{


  row.flag=true,
  row.saleAttrValue=''
}
//表单失去焦点获取数据
const toBlur=(row:SaleAttr)=>{
    const  {baseSaleAttrId,saleAttrValue}=row
    const newValue:SaleAttrValue={
      baseSaleAttrId,
      saleAttrValueName:(saleAttrValue as string)
    }
    //非法情况
 //非法情况一
    if((saleAttrValue as string).trim()==''){
    ElMessage({
       type:'error',
       message:'属性值不能为空'
     })
     return 
  }
  //非法情况二属性值不能相同
 const repeat= row.spuSaleAttrValueList.find((item)=>{
      return item.saleAttrValueName===saleAttrValue
    
  })
  if(repeat){

     ElMessage({
       type:'error',
       message:'属性值不能重复'
     })
           return 
  }

    row.spuSaleAttrValueList.push(newValue)
   row.flag=false
}

//删除销售属性中的属性值的tag和属性名
const DeleteSaleAtrrValue=(row,$index)=>{
    saleAttr.value.splice($index,1)
}

//计算还未存在的销售属性

const unSelectSaleAttr=computed(()=>{
  const unSelectAttr=allSaleAttr.value.filter(item=>{
    return saleAttr.value.every(item1=>{
      return item.name!=item1.saleAttrName
    })
  })
  return unSelectAttr
})

//点击保存
const save= async()=>{
  //处理图片
  SpuParams.value.spuImageList=imgList.value.map((item:any)=>{
    return {
      imgName:item.name,
      imgUrl:(item.response && item.response.data)||item.url
    }
  })
 //处理销售属性
 SpuParams.value.spuSaleAttrList=saleAttr.value
 //发请求
  const result= await reqAddOrUpdateSpu(SpuParams.value)
  
  if(result.code===200){
    ElMessage({
      type:'success',
      message:SpuParams.value.id?'更新成功':'添加成功'
    })
    //通知父组件切换
    $emit('changeScene',0)
  }else{
    ElMessage({
      type:'error',
      message:SpuParams.value.id?'更新失败':'添加失败'
    })
  }

  
}

defineExpose({initHasSpuData})
</script>
<style scoped lang="scss">

</style>

4.添加SPU模块业务
//子组件

//添加一个新的初始化请求方法
const initAddSpu= async(c3Id:number|string)=>{
  //情空
  Object.assign(SpuParams.value,{
  category3Id: "",
  description: "",
    spuName: "",
     tmId: '',
  //"id": 0,
  spuImageList: [],
  spuSaleAttrList: [],
 
})
//清空照片和销售属性
imgList.value=[]
saleAttr.value=[]
saleAttrIdAndValueName.value=''
  //储存三级分类的id
  SpuParams.value.category3Id=c3Id
  //获取品牌类型
 const result:AllTrademark=await reqAllTrademark()


 //获取销售属性的数据
 //获取整个销售属性
 const result3:HasSaleAttrRespnseData=await reqAllSaleAttr()
 allTradeMark.value=result.data
 allSaleAttr.value=result3.data

   

}
defineExpose({initHasSpuData,initAddSpu}) 
//父组件组件

//子组件通知父组件切换场景
const changeScene=(obj:any)=>{
     scene.value=obj.flag
     if(obj.params==='add'){
           getHasSpu()
          
     }else{
       getHasSpu(pageNo.value)
    
     }

}
5.添加SKU模块业务

1.静态页面搭建


<template>
  <el-form>
    <el-form-item label-width="100px"  label="sku名称"><el-input placeholder="SKU名称"></el-input></el-form-item>
    <el-form-item label-width="100px" label="价格(元)" ><el-input type="number" placeholder="价格(元)"></el-input></el-form-item>
    <el-form-item label-width="100px" label="重量(克)"><el-input type="number" placeholder="重量(克)"></el-input></el-form-item>
    <el-form-item label-width="100px" label="sku描述"><el-input  placeholder="sku描述" type="textarea"></el-input></el-form-item>
    <el-form-item label-width="100px" label="平台属性">
    <el-form :inline="true">
      <el-form-item :label="item.attrName" v-for="(item,index) in attrArr" :key="item.id">
            <el-select label="请选择">
        <el-option 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-width="100px" label="销售属性">
        <el-form :inline="true">
       <el-form-item :label="item.saleAttrName" v-for="(item,index) in saleArr" :key="item.id">
            <el-select label="请选择">
        <el-option 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-width="80px" label="图片名称">
      <el-table border :data="imgArr">
        <el-table-column type="selection" width="80px" align="center"></el-table-column>
        <el-table-column label="图片">
            <template #default="{row,$index}">
            <img :src="row.imgUrl" style="width:100px;height:100px" />
          </template>
        </el-table-column>
        <el-table-column label="名称" prop="imgName"></el-table-column>
        <el-table-column label="操作">
          <template #default="{row,$index}">
          <el-button type="primary" size="size" >设置默认</el-button>
          
          </template>
        </el-table-column>
        
      </el-table>
    </el-form-item>
    <el-form-item label-width="100px" >
      <el-button type="primary" size="small" >保存</el-button>
      <el-button type="primary" size="small"  @click="cancel">取消</el-button>
      
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { reqAttr } from "../../../api/product/attr"
import { reqSpuHasSaleAttr, reqspuImageList } from "../../../api/product/spu"
import {ref} from 'vue';

const $emit=defineEmits(['changeScene'])

//取消按钮
const cancel=()=>{
  $emit('changeScene',{flag:0,params:''})
}

//平台属性
const attrArr=ref<any>([])
//销售属性
const saleArr=ref<any>([])
//照片数据
const imgArr=ref<any>([])

//子组件对外暴露方法让父组件收到
const initSkuData= async(c1Id:number|string,c2Id:string|number,spu:any)=>{
     //获取平台属性
  const result=await reqAttr(c1Id,c2Id,spu.category3Id);
  attrArr.value==result.data
  //获取对应的销售属性
  const result1= await reqSpuHasSaleAttr(spu.id)
  saleArr.value=result1.data
  //获取照片墙的数据
  const result2=await reqspuImageList(spu.id)
  imgArr.value=result2.data

}

defineExpose({
  initSkuData
})
</script>
<style scoped lang="scss">
</style>

2.业务处理

  • 获取子组件的实例,ref和defineExpose的结合使用(重要)
  • 单独理解ref,reduce函数的使用

<template>
  <el-form>
    <el-form-item label-width="100px"  label="sku名称" ><el-input placeholder="SKU名称" v-model="skuParams.skuName"></el-input></el-form-item>
    <el-form-item label-width="100px" label="价格(元)"  ><el-input type="number" placeholder="价格(元)" v-model="skuParams.price"></el-input></el-form-item>
    <el-form-item label-width="100px" label="重量(克)" ><el-input type="number" placeholder="重量(克)" v-model="skuParams.weight"></el-input></el-form-item>
    <el-form-item label-width="100px" label="sku描述"><el-input  placeholder="sku描述" type="textarea" v-model="skuParams.skuDesc"></el-input></el-form-item>
    <el-form-item label-width="100px" label="平台属性">
    <el-form :inline="true"  >
      <el-form-item :label="item.attrName" v-for="(item,index) in attrArr" :key="item.id">
            <el-select label="请选择" v-model="item.attrIdAndValueId">
        <el-option v-for="(attrValue,index) in item.attrValueList" :key="attrValue.id" :label="attrValue.valueName" :value="`${item.id}:${attrValue.id}`"></el-option>
      </el-select> </el-form-item>
    
     
    </el-form>
    </el-form-item>
    <el-form-item label-width="100px" label="销售属性">
        <el-form :inline="true">
       <el-form-item :label="item.saleAttrName" v-for="(item,index) in saleArr" :key="item.id">
            <el-select label="请选择" v-model="item.saleIdAndValueId">
        <el-option v-for="(saleAttrValue,index) in item.spuSaleAttrValueList" :key="saleAttrValue.id" :label="saleAttrValue.saleAttrValueName" :value="`${item.id}:${saleAttrValue.id}`"></el-option>
      </el-select> </el-form-item>
    
     
    </el-form>
    </el-form-item>
    <el-form-item label-width="80px" 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 #default="{row,$index}">
            <img :src="row.imgUrl" style="width:100px;height:100px" />
          </template>
        </el-table-column>
        <el-table-column label="名称" prop="imgName"></el-table-column>
        <el-table-column label="操作">
          <template #default="{row,$index}">
          <el-button type="primary" size="size" @click="handler(row)" >设置默认</el-button>
          
          </template>
        </el-table-column>
        
      </el-table>
    </el-form-item>
    <el-form-item label-width="100px" >
      <el-button type="primary" size="small" @click="save">保存</el-button>
      <el-button type="primary" size="small"  @click="cancel">取消</el-button>
      
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { reqAttr } from "../../../api/product/attr"
import { reqAddSku, reqSpuHasSaleAttr, reqspuImageList } from "../../../api/product/spu"
import {reactive, ref} from 'vue';
import { SkuData } from "../../../api/product/spu/type";
import { ElMessage } from "element-plus";

const $emit=defineEmits(['changeScene'])

//取消按钮
const cancel=()=>{
  $emit('changeScene',{flag:0,params:''})
}

//平台属性
const attrArr=ref<any>([])
//销售属性
const saleArr=ref<any>([])
//照片数据
const imgArr=ref<any>([])
//收集的SKU参数
const skuParams=reactive<SkuData>({
     "category3Id":"",
     "spuId":"",
     "tmId":"",
     "skuName":"",
     "price":"",
     "weight":"",
     "skuDesc":"",
     //平台属性
     "skuAttrValueList":[],
     //销售属性
     "skuSaleAttrValueList":[],
     //图片地址
     "skuDefaultImg":""

 })
//获取组件实例
const table=ref<any>()
//子组件对外暴露方法让父组件收到
const initSkuData= async(c1Id:number|string,c2Id:string|number,spu:any)=>{
  //收集数据
  skuParams.category3Id=spu.category3Id,
  skuParams.spuId=spu.spuId//已有的SPUdeID
  skuParams.tmId=spu.tmId//SPU品牌的id
     //获取平台属性
  const result=await reqAttr(c1Id,c2Id,spu.category3Id);
  attrArr.value==result.data
  //获取对应的销售属性
  const result1= await reqSpuHasSaleAttr(spu.id)
  saleArr.value=result1.data
  //获取照片墙的数据
  const result2=await reqspuImageList(spu.id)
  imgArr.value=result2.data

}
//设置默认图片
const handler=(row)=>{
  //收集图片
  skuParams.skuDefaultImg=row.imgUrl
  //获取组件实例,实现选中复选框
//全部图片不勾选,排他思想
  imgArr.value.forEach((item:any) => {
     table.value.toggleRowSelection(item,false)
  });
    table.value.toggleRowSelection(row,true)

 
}
//保存
const save=async()=>{
      //多项处理
      const attrArrIds=attrArr.value.reduce((prev,next)=>{
          if(next.attrIdAndValueId){
           const [attrId,valueId]=  next.attrIdAndValueId.split(':')
            prev.push({attrId,valueId})
          }
          return prev
      },[])
      //平台属性的处理数据
      skuParams.skuAttrValueList=attrArrIds
      //销售属性的处理
      const saleArrIds=saleArr.value.reduce((prev,next)=>{
          if(next.saleIdAndValueId){
           const [saleAttrId,saleAttrValueId]=  next.saleIdAndValueId.split(':')
            prev.push({saleAttrId,saleAttrValueId})
          }
          return prev
      },[])
      skuParams.skuSaleAttrValueList=saleArrIds
      
 const result= await reqAddSku(skuParams)
if(result.code===200){
    ElMessage({
      type:'success',
      message:'添加成功'
    })
    //通知父组件切换
    $emit('changeScene',{flag:0,params:''})
  }else{
    ElMessage({
      type:'error',
      message:'添加失败'
    })
  }

}

defineExpose({
  initSkuData
})
</script>
<style scoped lang="scss">
</style>

6.sku查看信息模块业务
     <el-button size="small" type="primary" icon="View" title="查看SKU列表" @click="findSku(row)"></el-button>
     
       <!--对话框-->
       <el-dialog v-model="dialogTableVisible" title="SKU列表">
    <el-table :data="skuArr">
      <el-table-column label="sku名字" property="skuName"  ></el-table-column>
      <el-table-column label="sku价格" property="price" ></el-table-column>
      <el-table-column label="sku重量" property="weight"   ></el-table-column>
      <el-table-column label="sku图片"  >
        <template #default="{row,$index}" >
        <img  :src="row.skuDefaultImg" style="width:100px;height:100px" />
        </template>
      </el-table-column>
    </el-table>

  </el-dialog>
  
  
  //存放数据SKU
const skuArr=ref<SkuData[]>([])
//对话框
const dialogTableVisible=ref<boolean>(false)
//查看sKU
const findSku= async(row:SpuData)=>{
  const result= await reqSkuInfo(row.id)
 skuArr.value=result.data
 dialogTableVisible.value=true

}
7.删除模块信息
             <!--气泡框-->
      <el-popconfirm :title="`您确定要删除${row.spuName}`" width="250px" icon="Delete"  @confirm="deleteSpu(row)">
       <template #reference>
         <el-button size="small" type="primary" icon="Delete" title="删除SPU"></el-button>
       </template>
  </el-popconfirm>
            



//删除
const deleteSpu= async(row:SpuData)=>{
      
       const result=await reqRemoveSpu(row.id)
 
       if(result.code===200){
         ElMessage({
           type:'success',
           message:'删除成功'
         })
         //刷新一下
         getHasSpu(records.value.length>1? pageNo.value:pageNo.value-1)
       }else{
           ElMessage({
           type:'error',
           message:'删除失败'
         })
       }


}
6.SKU管理模块
1.静态搭建

<template>
  <div>
    <el-card style="margin: 15px 0">
      <el-table border :data="skuArr" setScrollLeft>
        <el-table-column label="序号" type="index" align="center" width="80px" ></el-table-column>
        <el-table-column label="名称"  width="150px" prop="skuName"></el-table-column>
        <el-table-column label="描述"  width="150px" prop="skuDesc"></el-table-column>
        <el-table-column label="默认图片" width="150px" >
          <template #default="{row,$index}">
            <img  style="width:100px;height:100px" :src="row.skuDefaultImg"/>
          </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="操作"  fixed="right"  >
           <template #default="{row,$index}" >
           <el-button size="small" type="primary" icon="Top" title="" @click="addSku(row)"></el-button>
           <el-button size="small" type="primary" icon="Edit" title="" @click="updateSpu(row)"></el-button>
            <el-button size="small" type="primary" icon="View" title="" @click="findSku(row)"></el-button>
                   <!--气泡框-->
      <el-popconfirm :title="`您确定要删除${row.spuName}`" width="250px" icon="Delete"  @confirm="deleteSpu(row)">
       <template #reference>
         <el-button size="small" type="primary" icon="Delete" title="删除SPU"></el-button>
       </template>
  </el-popconfirm>
            
          </template>
        </el-table-column>
      </el-table>
      <Pagination v-model:pageNo="pageNo" v-model:limit="limit" :getReq="getSkuList" :total="total"></Pagination>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import {onBeforeMount, ref, watch} from 'vue';
import { reqSkuList } from '../../../api/product/sku/index';
import { skuResponseData  } from '../../../api/product/sku/type';
//分页器的数据
const pageNo=ref(1)
const limit=ref(5)
const total=ref(10)
//sku数据
const skuArr=ref<skuResponseData[]>([])


onBeforeMount(()=>{
  getSkuList()
})


const getSkuList= async(pager=1)=>{

  pageNo.value=pager
   const result=await  reqSkuList(pageNo.value,limit.value)
 if(result.code===200){
   skuArr.value=result.data.records
   total.value=result.data.total
 }
}
</script>
<style scoped lang="scss">
</style>

2.上下架业处理
           <el-button size="small" type="primary" :icon="row.isSale==1?'Bottom':'Top'" @click="updateSale(row)" ></el-button>
           
           
           
           
//上下架更新
const updateSale= async(row:SkuData)=>{
  if(row.isSale===1){
    await reqCancelSaleSku(row.id)
    ElMessage({
      type:'success',
      message:'下架成功'
    })
    getSkuList(pageNo.value)
  }else{
      await reqSaleSku(row.id)
    ElMessage({
      type:'success',
      message:'上架成功'
    })
    getSkuList(pageNo.value)
  }
}
3.查看sku模块业务
  <el-button size="small" type="primary" icon="InfoFilled" @click="findSku(row)"></el-button>

el-row>
       <el-row style="margin:10px 0">
        <el-col :span="6">销售属性</el-col>
        <el-col :span="18"><el-tag type="danger" style="margin:5px 0"  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="height:100%;width:100%"/>
    </el-carousel-item>
  </el-carousel>
</el-col>
      </el-row>
    </template>
    <template #footer>
      <div style="flex: auto">
        <el-button @click="cancelClick">cancel</el-button>
        <el-button type="primary" @click="confirmClick">confirm</el-button>
      </div>
    </template>
  </el-drawer>
  
  //抽屉
const drawer=ref<any>(false)
const skuInfo=ref<SkuData[]>([])

//查看sku
const findSku= async(row:SkuData)=>{
    
    
  drawer.value=true
  const result= await reqSkuInfo(row.id)
  skuInfo.value=result.data
}
4.删除业务
                   <!--气泡框-->
      <el-popconfirm :title="`您确定要删除${row.spuName}`" width="250px" icon="Delete"  @confirm="deleteSku(row)">
       <template #reference>
         <el-button size="small" type="primary" icon="Delete" ></el-button>
       </template>
  </el-popconfirm>
  
  
  //删除
const deleteSku= async(row:any)=>{
     
     const result= await reqDeleteSku(row.id)
     if(result.code===200){
       ElMessage({
         type:'success',
         message:'删除成功'
       })
       getSkuList(skuArr.value.length>1?pageNo.value:pageNo.value-1)
     }else{
          ElMessage({
         type:'error',
         message:'删除失败'
       })
     }
}
6.用户管理模块
  • el-checkbox和el-checkbox-group的使用
  • 因为有些知识和上面的重复太多,所有就不具体分析了
<template>
  <div>

 <el-card  style="height:80px;">
  <el-form :inline="true" class="form">
    <el-form-item label="用户名:" >
      <el-input   placeholder="请输入用户名" v-model="keyword"/>
    </el-form-item> 
    <el-form-item >
  <el-button type="primary"  @click="searchUser">搜索</el-button>
  <el-button type="primary" @click="reset">重置</el-button>
    </el-form-item>
  </el-form>
    </el-card>
        <el-card style="margin:10px 0;">
        <el-button type="primary" size="small"  icon="Plus" @click="addUser">添加</el-button>
  <el-button type="danger" size="small" @click="deleteAllUser" :disabled="selectIdArr.length?false:true">批量删除</el-button>
  <el-table border  :data="userArr" @selection-change="selectionChange">
    <el-table-column type="selection" label="序号" align="center"  > </el-table-column>
      <el-table-column label="#" align="center" type="index"> </el-table-column>
      <el-table-column label="id" align="center" prop="id"> </el-table-column>
      <el-table-column label="用户名字" align="center" prop="username" show-overflow-tooltip> </el-table-column>
      <el-table-column label="用户名称" align="center" prop="name" show-overflow-tooltip> </el-table-column>
      <el-table-column label="用户角色" align="center" prop="roleName" show-overflow-tooltip> </el-table-column>
      <el-table-column label="创建时间" align="center" prop="createTime" show-overflow-tooltip> </el-table-column>
      <el-table-column label="更新时间" align="center" prop="updateTime" show-overflow-tooltip> </el-table-column>
      <el-table-column label="操作" width="300px" align="center"> 
            <template #default="{row,$index}">
              <el-button size="small" type="primary" icon="User" @click="setRole(row)"> 分配角色</el-button>
              <el-button size="small" type="primary" icon="Edit"  @click="updateUser(row)">编辑</el-button>
                                <!--气泡框-->
      <el-popconfirm :title="`您确定要删除${row.username}`" width="250px" icon="Delete"  @confirm="deleteUser(row.id)">
       <template #reference>
          <el-button size="small" type="primary" icon="Delete" >删除</el-button>
       </template>
  </el-popconfirm>
             
       </template>
      </el-table-column>
  </el-table>
    </el-card>
    <!--抽屉添加修改-->
    <el-drawer v-model="drawer" :direction="direction">
    <template #header>
      <h4>{
   
   {userParams.id?'添加用户':'更新用户'}}</h4>
    </template>
    <template #default>
     <el-form :model="userParams" :rules="rules"  ref="formRef">
       <el-form-item label="用户姓名" prop="username"><el-input  placeholder="请你输入用户姓名" v-model="userParams.username"></el-input></el-form-item>
        <el-form-item label="用户昵称" prop="name"><el-input  placeholder="请你输入用户昵称"  v-model="userParams.name" ></el-input></el-form-item>
                <el-form-item label="用户密码" prop="password" v-if="!userParams.id"><el-input  placeholder="请你输入用户密码" v-model="userParams.password"></el-input></el-form-item>
     </el-form>
    </template>
    <template #footer>
      <div style="flex: auto">
        <el-button   type="primary" @click="cancelClick">取消</el-button>
        <el-button type="primary" @click="save">确定</el-button>
      </div>
    </template>
  </el-drawer>
  <!--分页-->
     <Pagination v-model:pageNo="pageNo"  v-model:limit="limit"  :getReq="getHasUser" :total="total"></Pagination>
       <!--抽屉分配角色-->
    <el-drawer v-model="drawer2" :direction="direction">
    <template #header>
      <h4>分配角色</h4>
    </template>
    <template #default>
    <el-form :model="userParams" >
       <el-form-item label="用户姓名" ><el-input :disabled="!username"  v-model="userParams.username" > </el-input></el-form-item>
        <el-form-item label="角色列表">
          <el-checkbox v-model="checkedAll" :indeterminate="indeterminate" @change="handleCheckAllChange">全选</el-checkbox>
          <el-checkbox-group  @change="handleCheckedCitiesChange"  v-model="userRole">
          <el-checkbox v-for="(item,index) in allRole" :key="index"  :label="item">{
   
   {item.roleName}}</el-checkbox>
          </el-checkbox-group>
          
        </el-form-item>
              
     </el-form>
    </template>
    <template #footer>
      <div style="flex: auto">
        <el-button   type="primary" @click="cancelRole">取消</el-button>
        <el-button type="primary" @click="saveRole">确定</el-button>
      </div>
    </template>
  </el-drawer>
  </div>
</template>




<script setup lang="ts">
import {nextTick, onMounted, reactive, ref} from 'vue';
import { reqDeleteAllUser, reqDeleteUser, reqSetRole, reqUpdateRole, reqUserInfo } from '../../../api/acl/user/index';
import { AllRole, Records, SetRoleData, User} from '../../../api/acl/user/type';
import { reqAddOrUpdateUser } from '../../../api/acl/user/index';
import { ElMessage } from 'element-plus';
import useLayoutSettingStore from '../../../store/modules/setting';

const pageNo=ref<number>(1)
//页面个数
const limit=ref<number>(3)
//总数
const total=ref<number>(0)
//存储已有的用户数据
const userArr=ref<Records>([])
//抽屉
const drawer=ref<boolean>(false)
const drawer2=ref<boolean>(false)
//组件挂载
onMounted(()=>{
  getHasUser()
})
//收集用户信息的响应式数据
const userParams=reactive<User>({
  username: '',
  name: '',
  password: '',

})
//组件实例对象
const formRef=ref<any>()
//全选框
const checkedAll=ref<boolean>(false)
//设置不确定的样式
const indeterminate=ref<boolean>(true)
//储存全部职位的数据
const allRole=ref<AllRole>([])
//当前已有的职位的数据
const userRole=ref<AllRole>([])
//存储批量删除
const selectIdArr=ref<User[]>([])
//搜索用户
const keyword=ref<string>('')
//获取仓库的模板
const settingStore=useLayoutSettingStore()
const getHasUser= async(pager=1)=>{
   pageNo.value=pager


   const result=await reqUserInfo(pageNo.value,limit.value,keyword.value)
  if(result.code===200){
    total.value=result.data.total
    userArr.value=result.data.records

  }
}

//添加用户

const addUser=()=>{
  drawer.value=true
  //清空
  Object.assign(userParams,{
    id:0,
      username: '',
  name: '',
  password: '',
  })
  //清除效验
  nextTick(()=>{
    formRef.value.clearValidate('username')
       formRef.value.clearValidate('name')
   formRef.value.clearValidate('paswword')
  })

}

//编辑用户
const updateUser=(row:User)=>{
  drawer.value=true
  Object.assign(userParams,row)
   //清除效验
  nextTick(()=>{
    formRef.value.clearValidate('username')
       formRef.value.clearValidate('name')
 
  })
   
}

//保存

const save= async()=>{
    await formRef.value.validate()
    const result =await reqAddOrUpdateUser(userParams)
    if(result.code===200){
      drawer.value=false
      ElMessage({
        type:'success',
        message:userParams.id?'更新成功':'添加成功'
      })
      //getHasUser(userParams.id?pageNo.value:1)
      //浏览器自动刷新,改自己名的时候,名字也更新一下,立马回到第一页,上面的没必要
      window.location.reload()
    } else{
   drawer.value=false
      ElMessage({
        type:'error',
        message:userParams.id?'更新失败':'添加失败'
      })
    }
    }


//取消按钮
const cancelClick=()=>{
  drawer.value=false
}
//效验名的回调
const validatorUsername=(rule:any,value:any,callBack:any)=>{

    if(value.trim().length >=5){
      callBack()
    }else{
      callBack(new Error('用户名字至少为五位'))
    }
}

const validatorName=(rule:any,value:any,callBack:any)=>{
    if(value.trim().length>=5){
      callBack()
    }else{
      callBack(new Error('用户名字至少为五位'))
    }
}

const validatorPassword=(rule:any,value:any,callBack:any)=>{
    if(value.trim().length>=6){
      callBack()
    }else{
      callBack(new Error('用户名字至少为六位'))
    }
}

//规则效验
const rules={
  username:[{required:true,trigger:'blur',validator:validatorUsername}],
    name:[{required:true,trigger:'blur',validator:validatorName}],
      password:[{required:true,trigger:'blur',validator:validatorPassword}]
}

//分配角色
const setRole= async(row:User)=>{
  drawer2.value=true
  Object.assign(userParams,row)
  const result=await reqUpdateRole(row.id)
  if(result.code===200){
         allRole.value=result.data.allRolesList
          userRole.value=result.data.assignRoles

  }

}
//全选复选框的
const handleCheckAllChange=(val:boolean)=>{
  //如果val全选了,就把allrole赋给userRole,否则为空
       userRole.value= val?allRole.value:[]
       //有不确定变成确定的
       indeterminate.value=false
}
//多选框
const handleCheckedCitiesChange=(value:string[])=>{
        checkedAll.value= value.length===allRole.value.length
        //不等于为true,即为不确定状态
        indeterminate.value=value.length!==allRole.value.length

}

//角色取消按钮

const cancelRole=()=>{
  drawer2.value=false
}
//角色分配确定
const  saveRole=async()=>{
  //收集参数
  const data:SetRoleData={
    userId:userParams.id,
    roleIdList:userRole.value.map(item=>{
      return (item.id as number)
    })
  }
    const result=await reqSetRole(data)
    if(result.code===200){
      ElMessage({
        type:'success',
        message:'分配职位成功'
      })
      drawer2.value=false
      getHasUser(pageNo.value)
    }else{

       ElMessage({
        type:'error',
        message:'分配职位失败'
      })
    }
}

//删除用户
const deleteUser= async(userId:number)=>{
      const result= await reqDeleteUser(userId)
      if(result.code===200){
           ElMessage({
        type:'success',
        message:'删除成功'
      })
      getHasUser(userArr.value.length>1?pageNo.value:pageNo.value-1)
      }else{
             ElMessage({
        type:'success',
        message:'删除失败'
      })
      }
}

//table复选框勾选
const selectionChange=(value:any)=>{
 selectIdArr.value=value
}

//批量删除
const deleteAllUser= async()=>{
     const idList:any=selectIdArr.value.map(item=>{
       return item.id
     })
     const result=await reqDeleteAllUser(idList)
     if(result.code===200){
            ElMessage({
        type:'success',
        message:'批量删除成功'
      })
      getHasUser(userArr.value.length>1?pageNo.value:pageNo.value-1)
     }else{
             ElMessage({
        type:'success',
        message:'批量删除失败'
      })
      }
}

//点击搜索事件
const searchUser=()=>{
  getHasUser()
}

//点击重置
const reset=()=>{
  settingStore.refresh=!settingStore.refresh
}
</script>



<style scoped lang="scss">

.form{
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
}
</style>

7.角色管理模块

树形控件了解一下

树形控件递归勾选(重点)

 <el-tree-v2
    :data="menuArr"
    :props="defaultProps"
    node-key="id"
    show-checkbox
    :default-checked-keys="selectArr"
    :default-expanded-keys="defaultExpandedKeys"
    :height="650"
  />


  //准备一个数组:数组用于存储勾选的节点的ID(四级)
const selectArr=ref<number[]>([])
//方法
const filterSelectArr=(allData:any,initArr:any)=>{

 allData.forEach((item:any) => {
   //四级为true其他的都为真(一级)
    if(item.select&&item.level===4){
      initArr.push(item.id)
    }
    //子级
    if(item.children && item.children.length>0){
      filterSelectArr(item.children,initArr)
    }
    
  });

return initArr
}
     
     
  • 全部代码

  <template>
  <div>
 <el-card  style="height:80px;">
  <el-form :inline="true" class="form">
    <el-form-item label="职位名称:" >
      <el-input   placeholder="请输入搜索职位关键字"  v-model="keyword"/>
    </el-form-item> 
    <el-form-item >
  <el-button type="primary" :disabled="keyword?false:true" @click="searchRole">搜索</el-button>
  <el-button type="primary" @click="reset">重置</el-button>
    </el-form-item>
  </el-form>
    </el-card>
        <el-card style="margin:10px 0;">
        <el-button type="primary" size="small"  icon="Plus"  @click="addRole">添加角色</el-button>
  <el-table border :data="allRole" >
      <el-table-column label="#" align="center" type="index"> </el-table-column>
      <el-table-column label="id" align="center" prop="id" > </el-table-column>
      <el-table-column label="职位名称" align="center"  prop="roleName" show-overflow-tooltip> </el-table-column>
      <el-table-column label="创建时间" align="center"  prop="createTime" show-overflow-tooltip> </el-table-column>
      <el-table-column label="更新时间" align="center"  prop="updateTime" show-overflow-tooltip> </el-table-column>
      <el-table-column label="操作" width="300px" align="center"> 
            <template #default="{row,$index}">
              <el-button size="small" type="primary" icon="User"  @click="setPermisstion(row)"> 分配权限</el-button>
              <el-button size="small" type="primary" icon="Edit"  @click="updateRole(row)">编辑</el-button>
                                <!--气泡框-->
      <el-popconfirm :title="`您确定要删除${row.roleName}`" width="250px" icon="Delete" @confirm="removeRole(row.id)" >
       <template #reference>
          <el-button size="small" type="primary" icon="Delete" >删除</el-button>
       </template>
  </el-popconfirm>
             
       </template>
      </el-table-column>
  </el-table>
        </el-card>
  <!--分页-->
     <Pagination v-model:pageNo="pageNo"  v-model:limit="limit"  :getReq="getHasRole" :total="total"></Pagination>
     <!--对话框-->
       <el-dialog v-model="dialogTableVisible" :title=" RoleParams.id?'更新职位':'添加职位'">
    <el-form :model="RoleParams" :rules="rules"  ref="form">
      <el-form-item label="添加职位"  prop="roleName" >
        <el-input  v-model="RoleParams.roleName"/>
      </el-form-item>
     
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取消</el-button>
        <el-button type="primary" @click="saveRole">
          确定
        </el-button>
      </span>
    </template>
  </el-dialog>
   <!--抽屉-->
<el-drawer v-model="drawer">
    <template #header>
      <h4>分配权限</h4>
    </template>
    <template #default >
      <!--树形组件-->

  <el-tree-v2
    :data="menuArr"
    :props="defaultProps"
    node-key="id"
    show-checkbox
    :default-checked-keys="selectArr"
    :default-expanded-keys="defaultExpandedKeys"
    :height="650"
    ref="tree"
  />
     
    </template>
 
    <template #footer>
      <div style="flex: auto">
        <el-button @click="drawer=false">取消</el-button>
        <el-button type="primary" @click="confirmClick">确定</el-button>
      </div>
    </template>
  </el-drawer>
   
  </div>
</template>


<script setup lang="ts">
import { ElMessage } from 'element-plus';

import{nextTick, onMounted, reactive, ref} from 'vue';
import {reqAddORUpdateRole, reqAllMenuList, reqAllRoleList ,reqRemoveRole,reqSetPermisstion} from '../../../api/acl/role/index';
import { MenuList, Records, RoleData, } from '../../../api/acl/role/type';
import useLayoutSettingStore from '../../../store/modules/setting';
const pageNo=ref<number>(1)
//页面个数
const limit=ref<number>(3)
//总数
const total=ref<number>(0)
//存储所有的角色
const allRole=ref<Records>([])
//搜索
const keyword=ref<string>('')
//仓库刷新
const settingStore=useLayoutSettingStore()
//对话框
const dialogTableVisible=ref<boolean>(false)
//新增岗位
const RoleParams=reactive<RoleData>({
  roleName:''
})
//获取组件实例
const form =ref<any>()
//抽屉显示
const drawer=ref<boolean>(false)
//定义数组存储用户权限的数据
const menuArr=ref<MenuList>([])
//控制树形组件的测试数据局
const defaultProps={
      children:'children',
      label:'name'
}
onMounted(()=>{
  getHasRole()
})


const getHasRole= async(pager=1)=>{
  pageNo.value=pager
  const result =await reqAllRoleList(pageNo.value,limit.value,keyword.value)
  if(result.code===200){
    total.value=result.data.total
    allRole.value=result.data.records
  }
}

//搜索角色
const searchRole=()=>{
  getHasRole()
  keyword.value=''
}
//重置
const reset=()=>{
  settingStore.refresh=!settingStore.refresh
}
//新增角色
const addRole=()=>{
  dialogTableVisible.value=true
  //清空
  Object.assign(RoleParams,{
  roleName:'',
  id:0
})
//清空表单效验
nextTick(()=>{
  form.value.clearValidate('roleName')
})
}
//更新角色权限
const updateRole=(row:RoleData)=>{
  dialogTableVisible.value=true
  Object.assign(RoleParams,row)
  //清空表单效验
nextTick(()=>{
  form.value.clearValidate('roleName')
})
}


const validatorRoleName=(rule:any,value:any,callBack:any)=>{
   if(value.trim().length>=2){
     callBack()
   }else{
     callBack(new Error("职位名称至少两位"))
   }
}
//职位校验
const rules={
  roleName:[
    {required:true,trigger:'blur',validator:validatorRoleName}
  ]
}

//确认按钮的
const saveRole= async()=>{
   await form.value.validate()
       dialogTableVisible.value=false
   const result =await reqAddORUpdateRole(RoleParams)
   if(result.code===200){
 
     ElMessage({
       type:'success',
      message:RoleParams.id?'更新成功':'添加成功'
     })
     getHasRole(RoleParams.id?pageNo.value:1)
   }else{
       ElMessage({
       type:'success',
      message:RoleParams.id?'更新成功':'添加成功'
     })
   }
}
//设置分配权限
const setPermisstion= async(row:RoleData)=>{
  drawer.value=true
  Object.assign(RoleParams,row)
  const result=await reqAllMenuList(RoleParams.id)
  if(result.code===200){
    menuArr.value=result.data
   selectArr.value= filterSelectArr(menuArr.value,[])
  }
}
//抽屉权限保存
//树形组件实例
const tree=ref<any>()
const confirmClick= async()=>{
      const roleId=RoleParams.id
      //选择的节点ID
      const arr=tree.value.getCheckedKeys()
      //半选中的
      const arr1=tree.value.getHalfCheckedKeys()
      //合并
      const permissionId=arr.concat(arr1)

      //下发请求
       const result=await reqSetPermisstion(roleId,permissionId)
      if(result.code===200){
        drawer.value=false
        ElMessage({
          type:'success',
          message:'分配成功',
      
        })
            //刷新页面
          window.location.reload()
      }else{
          drawer.value=false
        ElMessage({
          type:'success',
          message:'分配失败',
      
        })
      }
   
        
}

//准备一个数组:数组用于存储勾选的节点的ID(四级)
const selectArr=ref<number[]>([])
//方法
const filterSelectArr=(allData:any,initArr:any)=>{

 allData.forEach((item:any) => {
   //四级为true其他的都为真(一级)
    if(item.select&&item.level===4){
      initArr.push(item.id)
    }
    //子级
    if(item.children && item.children.length>0){
      filterSelectArr(item.children,initArr)
    }
    
  });

return initArr
}
//删除角色
const removeRole=async(id:number)=>{
    const result=await reqRemoveRole(id)
      if(result.code===200){
        ElMessage({
            type:'success',
            message:'删除成功'
        })
        getHasRole(allRole.value.length>1?pageNo.value:pageNo.value-1)
      }else{
         ElMessage({
            type:'success',
            message:'删除失败'
        })
      }
    }

</script>
<style scoped lang="scss">

.form{
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
}
</style>

8.菜单管理模块
  • 全部代码
  • row-key=“id”

<template>
  <div>
   <el-table border :data="permissionArr" row-key="id" >
      <el-table-column label="名称"  prop="name"  show-overflow-tooltip> </el-table-column>
      <el-table-column label="权限值"   prop="code" show-overflow-tooltip> </el-table-column>
      <el-table-column label="修改时间" prop="updateTime" show-overflow-tooltip> </el-table-column>
      <el-table-column label="操作" width="300px"> 
            <template #default="{row,$index}">
              <el-button size="small" type="primary"  :disabled="row.level===4?true:false"  @click="addMenu(row)"> {
   
   {row.level==3?'添加功能':'添加菜单'}}</el-button>
              <el-button size="small" type="primary" :disabled="row.level===1?true:false" @click="updateMenu(row)"  >编辑</el-button>
           <!--气泡框-->
      <el-popconfirm :title="`您确定要删除${row.name}`" width="250px" icon="Delete" @confirm="removeMenu(row.id)" >
       <template #reference>
          <el-button size="small" type="primary" :disabled="row.level===1?true:false" >删除</el-button>
       </template>
  </el-popconfirm>
             
       </template>
      </el-table-column>
  </el-table>
   <!--对话框-->
       <el-dialog v-model="dialogTableVisible" :title="menuData.id?'更新菜单':'添加菜单'">
    <el-form >
      <el-form-item label="名称"  label-width="100px" >
        <el-input  type="small" placeholder="请输入菜单名称" v-model="menuData.name"/>
      </el-form-item>
      <el-form-item label="权限值"  label-width="100px"  >
        <el-input  type="small" placeholder="请输入权限值" v-model="menuData.code"/>
      </el-form-item>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取消</el-button>
        <el-button type="primary" @click="saveMenu">
          确定
        </el-button>
      </span>
    </template>
  </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ElMessage } from 'element-plus';
import {onMounted, reactive, ref} from 'vue';
import {reqAllPermisstion,reqAddOrUpdatePermisstion,reqRemovePermisstion} from '../../../api/acl/menu/index';
import {PermisstionList,MenuParams,PermissData} from '../../../api/acl/menu/type';


//显示对话框
const dialogTableVisible=ref<boolean>(false)
//添加修改携带的参数
const menuData=reactive<MenuParams>({

  code:'',
  level:0,
  name:'',
  pid:0

})
//存储已有的数据
const permissionArr=ref<PermisstionList>([])
onMounted(()=>{
  getHasMenu()
})


const getHasMenu= async()=>{
      const result=await reqAllPermisstion()
      if(result.code===200){
          permissionArr.value=result.data
      }
}

//添加菜单
const addMenu=(row:PermissData)=>{
  //清空
  Object.assign(menuData,{
  id:0,
  code:'',
  level:0,
  name:'',
  pid:0

  })
 dialogTableVisible.value=true
 //手机新增的菜单level
 menuData.level=row.level+1
 //给谁新增子菜单
 menuData.pid=row.id
 
}

//更新菜单
const updateMenu=(row:PermissData)=>{
  dialogTableVisible.value=true
  Object.assign(menuData,row)
}

//确定对话框
const saveMenu= async()=>{
    const result=await reqAddOrUpdatePermisstion(menuData)
   if(result.code===200){
     dialogTableVisible.value=false
     ElMessage({
       type:'success',
       message:menuData.id?'更新成功':'添加成功'
     })
     getHasMenu()
   }else{
      dialogTableVisible.value=false
     ElMessage({
       type:'success',
       message:menuData.id?'更新失败':'添加失败'
     })
   }
}

//删除菜单
const removeMenu= async(id:number)=>{
    const result =await reqRemovePermisstion(id)
    if(result.code===200){
      ElMessage({
        type:'success',
        message:"删除成功"
      })
      getHasMenu()
    }else{
        ElMessage({
        type:'error',
        message:"删除失败"
      })
    }
}
</script>
<style scoped lang="scss">

</style>

9.首页模块

<template>
<div>
    <el-card >
      <div class="box">
          <img :src="userStore.avatar" class="avatar"/>
          <div class="bottom1">
            <h3 class="title">{
   
   {getTime()}}好呀{
   
   {userStore.username}}</h3>
            <p class="subtitle">硅谷甄选运营平台</p>
          </div>
      </div>
    </el-card>
      <div class="bottoms">
      <SvgIcon name="welcome" width="600px" height="300px"></SvgIcon>
      </div>
</div>
</template>

<script setup lang="ts">
//引入用户头像和名称
import userUserStore from '../../store/modules/user';
import {getTime} from '../../util/time';
const userStore=userUserStore()



</script>
<style scoped lang="scss">
.box{
display: flex;

  .avatar{
    height:100px;
    width: 100px;
    border-radius: 50%;
  }
 .bottom1{
   
      margin-left: 20px;
    .title{
    font-size: 30px;
    font-weight: 900;
    margin-bottom: 30px;
    }
    .subtitle{
        font-style: italic;
        color: skyblue;
    }
  }
 
}


 .bottoms{

display: flex;
justify-content: center;
margin-top:20px;
 }
</style>

10.暗黑模块切换
  • 看element-plus的指南
//在main.js文件导入
//暗黑模式
import 'element-plus/theme-chalk/dark/css-vars.css'


<el-popover
    placement="bottom"
    title="主题设置"
    :width="300"
    trigger="hover">
  <!--表单元素-->
  <el-form>
        <el-form-item label="暗黑模式"  >
    <el-switch @change="changeDark" inline-prompt  v-model="dark" active-icon="MoonNight" inactive-icon="Sunny"/>
        </el-form-item>
  </el-form>
    <template #reference>
          <el-button  size="small" icon="Setting" circle ></el-button>
    </template>
  </el-popover>
 


//开关收集
const dark=ref<boolean>(false)
//暗黑开关
const changeDark=()=>{
  const html=document.documentElement
  dark.value?html.className='dark':html.className=''
}


11.更换主题颜色
  • 看element-plus的指南
  <el-popover
    placement="bottom"
    title="主题设置"
    :width="300"
    trigger="hover">
  <!--表单元素-->
  <el-form>
    <el-form-item label="主题颜色" >
    <el-color-picker @change="changeColor" v-model="color" show-alpha :predefine="predefineColors"  size="small"/>
    </el-form-item>
        <el-form-item label="暗黑模式"  >
    <el-switch @change="changeDark" inline-prompt  v-model="dark" active-icon="MoonNight" inactive-icon="Sunny"/>
        </el-form-item>
  </el-form>
    <template #reference>
          <el-button  size="small" icon="Setting" circle ></el-button>
    </template>
  </el-popover>


  
const color = ref('rgba(255, 69, 0, 0.68)')
const predefineColors = ref([
  '#ff4500',
  '#ff8c00',
  '#ffd700',
  '#90ee90',
  '#00ced1',
  '#1e90ff',
  '#c71585',
  'rgba(255, 69, 0, 0.68)',
  'rgb(255, 120, 0)',
  'hsv(51, 100, 98)',
  'hsva(120, 40, 94, 0.5)',
  'hsl(181, 100%, 37%)',
  'hsla(209, 100%, 56%, 0.73)',
  '#c7158577',
])

//主题颜色变化
const changeColor=()=>{
       const html=document.documentElement
       html.style.setProperty('--el-color-primary', color.value)
}
12.数据大屏模块
1.数据大屏解决适配问题vh和vw
  • 不能解决文字大小问题
  • 解决方案scale(有点难理解),有空隙
2.数据大屏顶部搭建
13.菜单权限和路由
1.拆分路由(三部分)
  • 静态路由(所有都可拥有)
    • login
    • 首页
    • 数据大屏
    • 404
  • 异步路由(不同身份不同的权限)
  • 任意路由
2.路由权限模块
1.先创建三部分的路由(任意路由、异步路由、静态路由)
2.业务处理
  • 出现二级路由需要(递归算法或map筛选)

  • 但是有二级使用赋值问题:(深拷贝)

    • 1.存在的问题刷新就没,2.用户登录路由重复

    • import { constantRoute ,asyncRoute,anyRoute} from "@/router/routes";
      
      
      //用户过滤菜单
      function filterAysncRoute(asyncRoute:any,routes:any){
          return  asyncRoute.filter((item:any)=>{
              if(routes.includes(item.name)){
                  if(item.children && item.children.length>0){
                 item.children=filterAysncRoute(item.children,routes)
                  }
                  return true
              }
          })
      }
      
        const userAsyncRoute = filterAysncRoute(asyncRoute,result.data.routes);
                  
                   this.menuRoutes = [...constantRoute,...userAsyncRoute, ...anyRoute];
      //注册路由非常重要
                    [...userAsyncRoute, ...anyRoute].forEach((route:any)=>{
                       router.addRoute(route)
                   })
      
3.解决的两个问题
  • lodash(深拷贝)(解决用户登录路由重复)

  • 在路由守卫添加代码(解决刷新问题)

        await  userStore.userInfo()
                    //万一刷新是异步路由的
    
                    //next()
                    next({...to})
    
4.按钮权限模块
  • 可以使用v-if,但是繁琐和麻烦

  • 使用自定义指令

    //自定义指令
    import pinia from '@/store';
    import userUserStore from '@/store/modules/user';
    const userStore=userUserStore()
    export const isHasButton=(app:any)=>{
      //全局自定义指令
      app.directive('has',{
          mounted(el:any,options:any) {
              if(userStore.buttons.includes(options.value)){
                  el.parentNode.removeChild(el)
              }
          },
      })
    }
    
    
       <el-button size="default" type="primary" icon="Plus" @click="addTrademark" v-has="`btn.Trademark.add`">添加品牌</el-button>
    
14.项目打包

npm run build

按照提示去修改
“bottom1”>

{ {getTime()}}好呀{ {userStore.username}}


硅谷甄选运营平台








#### 10.暗黑模块切换

- 看element-plus的指南

```vue
//在main.js文件导入
//暗黑模式
import 'element-plus/theme-chalk/dark/css-vars.css'


<el-popover
    placement="bottom"
    title="主题设置"
    :width="300"
    trigger="hover">
  <!--表单元素-->
  <el-form>
        <el-form-item label="暗黑模式"  >
    <el-switch @change="changeDark" inline-prompt  v-model="dark" active-icon="MoonNight" inactive-icon="Sunny"/>
        </el-form-item>
  </el-form>
    <template #reference>
          <el-button  size="small" icon="Setting" circle ></el-button>
    </template>
  </el-popover>
 


//开关收集
const dark=ref<boolean>(false)
//暗黑开关
const changeDark=()=>{
  const html=document.documentElement
  dark.value?html.className='dark':html.className=''
}


11.更换主题颜色
  • 看element-plus的指南
  <el-popover
    placement="bottom"
    title="主题设置"
    :width="300"
    trigger="hover">
  <!--表单元素-->
  <el-form>
    <el-form-item label="主题颜色" >
    <el-color-picker @change="changeColor" v-model="color" show-alpha :predefine="predefineColors"  size="small"/>
    </el-form-item>
        <el-form-item label="暗黑模式"  >
    <el-switch @change="changeDark" inline-prompt  v-model="dark" active-icon="MoonNight" inactive-icon="Sunny"/>
        </el-form-item>
  </el-form>
    <template #reference>
          <el-button  size="small" icon="Setting" circle ></el-button>
    </template>
  </el-popover>


  
const color = ref('rgba(255, 69, 0, 0.68)')
const predefineColors = ref([
  '#ff4500',
  '#ff8c00',
  '#ffd700',
  '#90ee90',
  '#00ced1',
  '#1e90ff',
  '#c71585',
  'rgba(255, 69, 0, 0.68)',
  'rgb(255, 120, 0)',
  'hsv(51, 100, 98)',
  'hsva(120, 40, 94, 0.5)',
  'hsl(181, 100%, 37%)',
  'hsla(209, 100%, 56%, 0.73)',
  '#c7158577',
])

//主题颜色变化
const changeColor=()=>{
       const html=document.documentElement
       html.style.setProperty('--el-color-primary', color.value)
}
12.数据大屏模块
1.数据大屏解决适配问题vh和vw
  • 不能解决文字大小问题
  • 解决方案scale(有点难理解),有空隙
2.数据大屏顶部搭建
13.菜单权限和路由
1.拆分路由(三部分)
  • 静态路由(所有都可拥有)
    • login
    • 首页
    • 数据大屏
    • 404
  • 异步路由(不同身份不同的权限)
  • 任意路由
2.路由权限模块
1.先创建三部分的路由(任意路由、异步路由、静态路由)
2.业务处理
  • 出现二级路由需要(递归算法或map筛选)

  • 但是有二级使用赋值问题:(深拷贝)

    • 1.存在的问题刷新就没,2.用户登录路由重复

    • import { constantRoute ,asyncRoute,anyRoute} from "@/router/routes";
      
      
      //用户过滤菜单
      function filterAysncRoute(asyncRoute:any,routes:any){
          return  asyncRoute.filter((item:any)=>{
              if(routes.includes(item.name)){
                  if(item.children && item.children.length>0){
                 item.children=filterAysncRoute(item.children,routes)
                  }
                  return true
              }
          })
      }
      
        const userAsyncRoute = filterAysncRoute(asyncRoute,result.data.routes);
                  
                   this.menuRoutes = [...constantRoute,...userAsyncRoute, ...anyRoute];
      //注册路由非常重要
                    [...userAsyncRoute, ...anyRoute].forEach((route:any)=>{
                       router.addRoute(route)
                   })
      
3.解决的两个问题
  • lodash(深拷贝)(解决用户登录路由重复)

  • 在路由守卫添加代码(解决刷新问题)

        await  userStore.userInfo()
                    //万一刷新是异步路由的
    
                    //next()
                    next({...to})
    
4.按钮权限模块
  • 可以使用v-if,但是繁琐和麻烦

  • 使用自定义指令

    //自定义指令
    import pinia from '@/store';
    import userUserStore from '@/store/modules/user';
    const userStore=userUserStore()
    export const isHasButton=(app:any)=>{
      //全局自定义指令
      app.directive('has',{
          mounted(el:any,options:any) {
              if(userStore.buttons.includes(options.value)){
                  el.parentNode.removeChild(el)
              }
          },
      })
    }
    
    
       <el-button size="default" type="primary" icon="Plus" @click="addTrademark" v-has="`btn.Trademark.add`">添加品牌</el-button>
    
14.项目打包

npm run build

按照提示去修改

猜你喜欢

转载自blog.csdn.net/m0_63084496/article/details/132624968
今日推荐