返回

Vue项目实战

本项目采用最新的Vue3+组合式API开发方式

使用主流技术栈:vue3+typescript+vue-router+pinia+element-plus+axios+echarts

GitHub仓库地址

初始化项目

环境准备:

  • node v18.17.0
  • pnpm v8.6.12

pnpm安装:参考

  1. 使用vite构建项目:
1
pnpm create vite
  1. 进入项目目录后安装依赖:
1
pnpm install
  1. 启动项目
1
pnpm run dev --host

  1. 删除默认的/src/style.css文件,同时在main.ts中也删除
  2. 安装Vue VSCode Snippets扩展
  3. 删除默认的App.Vue文件内容,输入v3ts选组合式API生成模板后修改:
1
2
3
4
5
<template>
  <div>
    <h1>App根组件</h1>
  </div>
</template>
  1. 删除自带的/src/components/HelloWorld.vue组件和/src/assets/vue.svg图标
  2. 修改页面标题index.html
1
<title>Vue3-template</title>
  1. VSCode中搜索并安装扩展:Volar
  2. 设置Volar Takeover 模式
  3. tsconfig.jsontsconfig.node.json中的moduleResolution选项设置为nodeCTRL+SHIFT+PRELOAD WINDOW

项目集成

集成element-plus

  1. 安装依赖
1
pnpm i element-plus
  1. 在入口文件main.ts中注册插件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { createApp } from 'vue'
import App from './App.vue'
//引入element-plus 插件与样式
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

//获取应用实例对象
const app = createApp(App)
//注册element-plus插件
app.use(ElementPlus)
//将应用挂载到挂载点上
app.mount('#app')
  1. 安装图标组件库
1
pnpm i @element-plus/icons-vue
  1. 安装Element UI Snippets扩展
  2. main.ts配置element组件使用中文
1
2
3
4
5
6
7
//配置element-plus中文
//@ts-ignore忽略当前文件ts类型的检测否则有红色提示(打包会失败)
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
////注册element-plus插件
app.use(ElementPlus, {
    locale: zhCn, //配置element-plus中文
  })
  1. App.vue中测试效果:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<template>
  <div>
    <el-button type="primary" size="default" :icon="Plus">主要按钮</el-button>
    <el-button type="success" size="small" :icon="Edit">编辑按钮</el-button>
    <el-button type="danger" size="default" :icon="Delete">删除按钮</el-button>
    <el-pagination
      :page-sizes="[100, 200, 300, 400]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="400"
    />
  </div>
</template>

<script setup lang="ts">
//引入图表组件
import { Plus,Edit,Delete } from '@element-plus/icons-vue'
</script>

<style scoped></style>

src别名配置

  1. 安装依赖
1
pnpm install @types/node
  1. vite.config.ts中引入path
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
    }
  }
})
  1. TypeScript编译配置
1
2
3
4
5
6
7
8
9
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
    "paths": { //路径映射,相对于baseUrl
      "@/*": ["src/*"] 
    }
  }
}
  1. main.ts中使用@引入App.vue
1
import App from '@/App.vue'
  1. 新建@/components/Test.vue测试组件@
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
    <div>
        <h1>测试组件@</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped></style>
  1. App.vue直接引用测试组件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
  <div>
    <el-button type="primary" size="default" :icon="Plus">主要按钮</el-button>
    <el-button type="success" size="small" :icon="Edit">编辑按钮</el-button>
    <el-button type="danger" size="default" :icon="Delete">删除按钮</el-button>
    <el-pagination
      :page-sizes="[100, 200, 300, 400]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="400"
    />
    <!-- 引入测试组件 -->
    <Test></Test>
  </div>
</template>

<script setup lang="ts">
//引入图表组件
import { Plus,Edit,Delete } from '@element-plus/icons-vue'
import Test from '@/components/Test.vue'//引入测试组件
</script>

<style scoped></style>

环境变量的配置

  1. 项目根目录分别添加 开发、生产和测试环境的文件
1
2
3
.env.development
.env.production
.env.test
  1. 文件内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_TITLE = 'xxxx'
VITE_APP_BASE_API = '/api'
VITE_SERVE="http://xxx"

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'production'
VITE_APP_TITLE = 'xxx系统'
VITE_APP_BASE_API = '/prod-api'
VITE_SERVE="http://xxx"

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'test'
VITE_APP_TITLE = 'xxx系统'
VITE_APP_BASE_API = '/test-api'
VITE_SERVE="http://xxx"
  1. 配置运行命令:package.json
1
2
3
4
5
6
"scripts": {
    "dev": "vite --open",
    "build:test": "vue-tsc && vite build --mode test",
    "build:pro": "vue-tsc && vite build --mode production",
    "preview": "vite preview"
  },
  1. 在项目中可以通过import.meta.env获取环境变量

SVG图标配置

在开发项目的时候经常会用到svg矢量图,而且我们使用SVG以后,页面上加载的不再是图片资源,这对页面性能来说是个很大的提升,而且我们SVG文件比img要小的很多,放在项目中几乎不占用资源。

  1. 安装SVG依赖插件
1
2
pnpm install vite-plugin-svg-icons -D
pnpm install fast-glob
  1. vite.config.ts中配置插件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

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]',
      }),
    ],
  }
}
  1. main.ts入口文件中导入
1
import 'virtual:svg-icons-register'
  1. 新建src/assets/icons,导入用到的svg图标

  2. svg封装为全局组件:因为项目很多模块需要使用图标,因此把它封装为全局组件,在src/components目录下创建一个SvgIcon文件夹,在SvgIcon文件夹下再新建index.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
    <div>
        <svg :style="{ width: width, height: height }">
            <use :xlink:href="prefix + name" :fill="color"></use>
        </svg>
    </div>
</template>
  
<script setup lang="ts">
defineProps({
    //xlink:href属性值的前缀
    prefix: {
        type: String,
        default: '#icon-'
    },
    //svg矢量图的名字
    name: String,
    //svg图标的颜色
    color: {
        type: String,
        default: ""
    },
    //svg宽度
    width: {
        type: String,
        default: '16px'
    },
    //svg高度
    height: {
        type: String,
        default: '16px'
    }

})
</script>
<style scoped></style>
  1. 修改App.vue测试SVG,删除之前测试@创建的Test.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<template>
  <div>
    <h1>测试SVG</h1>
    <!-- 测试SVG图标使用 -->
    <svg-icon name="welcome"></svg-icon>
  </div>
</template>

<script setup lang="ts">
import SvgIcon from '@/components/SvgIcon/index.vue'
</script>

<style scoped>

</style>

使用自定义插件方式注册SVG为全局组件

注册为全局组件后就不用在每次使用的时候都用import引入

  1. src/components文件夹下创建index.ts文件:用于注册components文件夹内部全部全局组件(./Pagination/index.vue直接用v3ts模板显示一句话就行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//引入项目中全部的全局组件
import SvgIcon from './SvgIcon/index.vue';
import Pagination from './Pagination/index.vue';
import type { App, Component } from 'vue';
//全局对象
const allGlobalComponents: { [name: string]: Component } = { SvgIcon, Pagination };
export default {
    //务必叫做install方法
    install(app: App) {
        //注册项目全部的全局组件
        Object.keys(allGlobalComponents).forEach((key: string) => {
            //注册为全局组件
            app.component(key, allGlobalComponents[key]);
        })
    }
}
  1. main.ts入口文件中引入src/index.ts文件,通过app.use方法安装自定义插件
1
2
import gloablComponent from '@/components';
app.use(gloablComponent);
  1. App.vue中直接使用全局组件,不用import导入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<template>
  <div>
    <h1>测试SVG</h1>
    <!-- 测试SVG图标使用 -->
    <svg-icon name="welcome"></svg-icon>
    <!-- 测试自定义组件方式注册的全局组件 -->
    <Pagination />
  </div>
</template>

<script setup lang="ts">
// import SvgIcon from '@/components/SvgIcon/index.vue'
</script>

<style scoped></style>

集成sass

  1. 安装依赖
1
pnpm install sass sas-loader
  1. App.vue中测试项目能否使用scss语法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<template>
  <div>
<h1>测试SCSS</h1>
  </div>
</template>

<script setup lang="ts">

</script>

<style scoped lang="scss">
  div{
    h1{
      color: red;
    }
  }
</style>
  1. 为项目添加全局样式:在src/styles下新建index.scss文件和reset.scss,在npm官网搜索reset.scss后复制内容到项目文件中,项目中可能需要用到清除默认样式,因此在index.scss中引入reset.scss
1
2
//引入q默认样式
@import './reset.scss';
  1. main.ts入口文件引入全局样式
1
import '@/styles/index.scss'
  1. 给项目中引入全局变量:在styles下创建一个variable.scss文件,在vite.config.ts文件配置如下
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export default defineConfig((config) => {
    css: {
      preprocessorOptions: {
        scss: {
          javascriptEnabled: true,
          additionalData: '@import "./src/styles/variable.scss";',
        },
      },
    },
  }
}
  1. variable.scss下设置全局变量
1
2
3
4
//为项目提供scss全局变量

//定义项目主题颜色
$base-color: purple;
  1. App.vue中使用全局变量
1
2
3
4
5
6
7
<style scoped lang="scss">
div {
  h1 {
    color: $base-color;
  }
}
</style>

mock数据

  1. 安装依赖
1
pnpm install -D vite-plugin-mock@2.9.6 mockjs
  1. vite.config.js配置文件启用插件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig(({ command }) => {
  return {
    plugins: [
    ...,
    viteMockServe({
      //保证开发阶段可以使用mock数据
      localEnabled: command === 'serve',
    })
   	],
    ...
})
  1. 在根目录创建mock文件夹:在mock文件夹内部创建一个user.ts文件,写入需要的mock数据与接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//用户信息数据
//createUserList此函数执行会返回一个用户信息数组,包含两个用户信息
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 } }
        },
    },
]
  1. 安装axios
1
pnpm i axios
  1. main.ts测试mock能否使用(测试完后删除)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//测试代码:测试mock数据和接口能否使用
import axios from 'axios'
//登录接口
axios({
  url: '/api/user/login',//请求地址
  method: 'post',//请求方式
  data: {
    username: 'admin',
    password: '111111'
  }
})

axios二次封装

在开发项目的时候避免不了与后端进行交互,因此需要使用axios插件实现发送网络请求。在开发项目的时候经常会把axios进行二次封装。

  • 使用请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)
  • 使用响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)
  1. src目录下创建utils/request.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//进行axios二次封装:使用请求与响应拦截器
import axios from 'axios'
import { ElMessage } from 'element-plus'
// 1.利用axios对象的create方法创建一个新的axios实例
let request = axios.create({
    // 配置基础路径
    baseURL: import.meta.env.VITE_APP_BASE_API,//基础路径上会携带/api
    timeout: 5000//超时设置
})
//2.请求拦截器
request.interceptors.request.use((config) => {
    //config是请求配置对象,headers是请求头对象,经常给服务器端传递token
    //返回配置对象
    return config
})

//3.响应拦截器
request.interceptors.response.use((response) => {
    //成功回调
    //简化数据:在/mock/user.ts中可以看到响应数据是code和data,响应拦截器只返回data即可
    return response.data
}, (error) => {
    //失败回调:处理http网络错误
    //定义一个变量:存储网络错误信息
    let message = '';
    //http状态码判断
    let statusCode = error.response.status;
    switch (statusCode) {
        case 401:
            message = 'token过期';
            break;
        case 403:
            message = '无权访问';
            break;
        case 404:
            message = '请求资源不存在';
            break;
        case 500:
            message = '服务器内部错误';
            break;
        default:
            message = '网络错误';
            break;
    }
    //提示错误信息
    ElMessage({
        type: 'error',
        message
    })
    //返回一个失败的promise对象
    return Promise.reject(error)
})

//4.导出axios实例 对外暴露
export default request
  1. App.vue测试(测试完后删除)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
  <div>
    <h1>测试axios二次封装</h1>
  </div>
</template>

<script setup lang="ts">
import request from './utils/request';
import { onMounted } from 'vue';
//当组件挂载完成后执行
onMounted(() => {
  request({
    url: '/user/login',
    method: 'post',
    data: {
      username: 'admin',
      password: '111111'
    }
  }).then(res => {
    console.log(res);
  })
})
</script>

<style scoped></style>

有真实接口的可以在这直接跳到:完善部分功能——直接使用真实接口

API接口统一管理

在开发项目的时候接口可能很多,因此需要统一管理

  1. src目录下创建api/user/index.tsapi/user/type.ts文件统一管理用户登录、用户信息获取相关的接口和数据类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//统一管理项目用户相关的接口
import request from '@/utils/request'
import type { LoginParams, LoginResultModel, UserInfoModel } from './type'
//统一管理接口
enum API {
    LOGIN_URL = '/user/login',//登录接口
    USERINFO_URL = '/user/info',//获取用户信息接口
}
//对外暴露请求函数
//登录接口方法
export const reqLogin = (data: LoginParams) => request.post<any, LoginResultModel>(API.LOGIN_URL, data)
//获取用户信息接口方法
export const reqUserInfo = () => request.get<any, UserInfoModel>(API.USERINFO_URL)
  1. types.ts嫌麻烦可以不用,直接在上面用any就行,但是后期维护不方便
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//登录接口需要携带参数ts类型
export interface LoginParams {
    username: string,
    password: string
}

interface dataType {
    token?: string
    message?: string
}

//登录接口返回数据类型
export interface LoginResultModel {
    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 UserInfoModel {
    code: number,
    data: user
}
  1. App.vue中测试(测试结束后删除)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<template>
  <div>
    <h1>App根组件</h1>
  </div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue';
import { reqLogin } from './api/user';
onMounted(() => {
  reqLogin({ username: 'admin', password: '111111' })
}
)
</script>

<style scoped></style>

路由配置

  1. 安装依赖
1
pnpm install vue-router@4.1.6
  1. 新建src/views文件夹专门放置路由的页面,/views下新建loginhome404目录
  2. login下新建index.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
    <div>
        <h1>我是一级路由登录</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped></style>
  1. home下新建index.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
    <div>
        <h1>我是一级路由展示登录成功以后的数据</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped></style>
  1. 404下新建index.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
    <div>
        <h1>我是一级路由404</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped></style>
  1. 新建src/router/routers.ts文件夹专门放置路由表
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//对外暴露配置路由(常量路由)
export const constantRoutes = [
    {
        //登录
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login'//命名路由,后面做权限管理时用到
    }
    ,
    {
        //登录成功以后展示数据的路由
        path: '/',
        component: () => import('@/views/home/index.vue'),
        name: 'home'//命名路由,后面做权限管理时用到
    }
    ,
    {
        //404
        path: '/404',
        component: () => import('@/views/404/index.vue'),
        name: '404'//命名路由,后面做权限管理时用到
    }
    ,
    {
        //任意路由
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        name: 'Any'

    }
]
  1. 新建src/router/index.ts实现模板路由配置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//通过vue-router插件实现模板路由配置
import { createRouter, createWebHashHistory } from 'vue-router'
import { constantRoutes } from './routers'
//创建路由器
let router = createRouter({
    //路由模式hash
    history: createWebHashHistory(),
    routes: constantRoutes,
    //滚动行为
    scrollBehavior() {
        return {
            left: 0,
            top: 0
        }
    }
})

export default router

  1. main.ts中引入
1
2
3
4
//引入路由
import router from './router';
//注册模板路由
app.use(router);
  1. App.vue中展示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<template>
  <div>
    <router-view></router-view>
  </div>
</template>

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

<style scoped></style>

登录模块

  1. 安装仓库依赖pinia
1
pnpm i pinia
  1. 新建src\store\index.ts
1
2
3
4
5
6
//仓库大仓库
import {createPinia} from 'pinia'
//创建大仓库
let pinia = createPinia()
//导出大仓库:入口文件需要安装仓库
export default pinia
  1. 在入口文件main.ts引入仓库
1
2
3
4
//引入仓库
import pinia from './store';
//安装仓库pinia
app.use(pinia);
  1. 新建/src/utils/storage.ts封装本地存储的读取与存储方法
1
2
3
4
5
6
7
8
9
//封装本地存储的读取与存储方法
//本地存储存储TOKEN
export const SET_TOKEN = (token: string) => {
    localStorage.setItem('TOKEN', token)
}
//本地存储读取TOKEN
export const GET_TOKEN = () => {
    return localStorage.getItem("TOKEN")
}
  1. 创建用户仓库/store/moudules/types/type.ts声明用到的数据类型
1
2
3
4
//定义小仓库数据state类型
export interface UserState {
    token: string | null
}
  1. 新建/src/utils/time.ts获取一个结果:当前早上|上午|下午|晚上
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//封装一个函数:获取一个结果:当前早上|上午|下午|晚上
export const getTime = () => {
    let message = ''
    //使用内置构造函数Date()获取当前时间
    let hours = new Date().getHours()
    //判断当前时间
    if (hours >= 6 && hours < 9) {
        message = '早上'
    } else if (hours >= 9 && hours < 12) {
        message = '上午'
    } else if (hours >= 12 && hours < 18) {
        message = '下午'
    } else {
        message = '晚上'
    }
    return message
}
  1. 创建用户仓库/store/moudules/user.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//创建用户相关的小仓库
import { defineStore } from 'pinia'
//引入接口
import { reqLogin } from '@/api/user'
//引入数据类型
import type { LoginParams, LoginResultModel } from '@/api/user/type'
import type { UserState } from './types/type'
//引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN } from '@/utils/storage'
//创建用户小仓库
let useUserStore = defineStore(
    //小仓库的名字
    'User', {
    //小仓库存储数据的地方
    state: (): UserState => {
        return {
            token: GET_TOKEN(),//用户唯一标识
        }
    },
    //处理异步和逻辑的地方
    actions: {
        //用户登录的方法
        async login(data: LoginParams) {
            //登录请求
            let result: LoginResultModel = await reqLogin(data)
            //登录请求:成功200->token
            //登录请求:失败201->message
            if (result.code === 200) {
                //登录成功
                //pinia仓库存储一下token
                //由于pinia存储数据利用的是js对象,所以未持久化
                this.token = (result.data.token as string)
                //本地存储持久化token
                SET_TOKEN((result.data.token as string))
                //保证当前async函数返回值是一个成功的promise
                return 'ok'
            } else {
                //登录失败
                return Promise.reject(new Error(result.data.message))
            }

        }
    },
    getters: {

    }
})
//对外暴露获取小仓库的方法
export default useUserStore
  1. 登录路由静态组件src\views\login\index.vue
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
<template>
    <div class="login_container">
        <el-row>
            <el-col :span="12" :xs="0"></el-col>
            <el-col :span="12">
                <el-form class="login_form">
                    <h1>你好</h1>
                    <h2>欢迎来到xxx系统</h2>
                    <el-form-item>
                        <el-input :prefix-icon="User" v-model="loginFrom.username"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-input type="password" :prefix-icon="Lock" v-model="loginFrom.password" show-password></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-button :loading="loading" class="login_btn" type="primary" size="default"
                            @click="login">登录</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, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElNotification } from 'element-plus';
//引入用户相关的小仓库
import useUserStore from '@/store/modules/user';
//引入获取当前时间的函数
import { getTime } from '@/utils/time';
let userStore = useUserStore()
//获取路由器
let $router = useRouter()
//定义变量控制按钮加载效果
let loading = ref(false)
//收集账号与密码数据
let loginFrom = reactive({
    //默认值
    username: 'admin',
    password: '111111'
})
//点击了登录按钮,登录按钮回调
const login = async () => {
    //按钮加载效果:开始加载
    loading.value = true
    // 通知仓库发送登录请求
    try {
        //请求成功->进入首页展示数据
        await userStore.login(loginFrom)
        //编程式导航跳转到展示数据首页
        $router.push('/')
        //登录成功的提示信息
        ElNotification({
            type: 'success',
            title: `嗨,${getTime()}好`,
            message: '欢迎回来'
        })
        //登录成功后,按钮加载效果:结束加载
        loading.value = false
    } catch (error) {
        //请求失败->弹出登陆失败信息
        //按钮加载效果:结束加载
        loading.value = false
        //登录失败的提示信息
        ElNotification({
            type: 'error',
            title: '登录失败',
            message: (error as Error).message
        })
    }
}
</script>

<style scoped lang="scss">
.login_container {
    width: 100%;
    height: 100vh;
    background: url('@/assets/images/background.jpg') no-repeat;
    background-size: cover;
}

.login_form {
    position: relative;
    width: 80%;
    top: 30vh;
    background: url('@/assets/images/login_form.png') no-repeat;
    background-size: cover;
    padding: 40px;

    h1 {
        color: white;
        font-size: 40px;
    }

    h2 {
        color: white;
        font-size: 20px;
        margin: 20px 0;
    }

    .login_btn {
        width: 100%;
    }
}
</style>

表单数据校验

Form 表单 | Element Plus (element-plus.org):Form 组件允许你验证用户的输入是否符合规范,来帮助你找到和纠正错误。

Form 组件提供了表单验证的功能,只需为 rules 属性传入约定的验证规则,并将 form-Itemprop 属性设置为需要验证的特殊键值即可

  1. src\views\login\index.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<el-form class="login_form" :model="loginFrom" :rules="rules" ref = "loginForms">
    <el-form-item prop="username"></el-form-item>
    <el-form-item prop="password"></el-form-item>
    </el-form-item> 
</template>
<script setup lang="ts">  
//获取el-form组件
let loginForms = ref()
//点击了登录按钮,登录按钮回调
const login = async () => {
    await loginForms.value.validate()
    ...
}
//定义表单校验需要的配置对象
const rules = {
    //规则对象属性
    username: [
        {
            required: true, // required,代表这个字段务必要校验的
            min: 6, //min:文本长度至少多少位
            max: 10, // max:文本长度最多多少位
            message: '账号长度至少六位', // message:错误的提示信息
            trigger: 'change' //trigger:触发校验表单的时机 change->文本发生变化触发校验, blur:失去焦点的时候触发校验规则
        }
    ],
    password: [
        {
            required: true,
            min: 6,
            max: 15,
            message: '密码长度至少六位',
            trigger: 'change'
        }
    ]
}
</srcipt> 
  1. 自定义校验规则:src\views\login\index.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">  
//自定义校验规则需要的函数
const validatorUserName = (rule: any, value: any, callback: any) => {
    //rule:当前校验规则对象
    //value:当前表单项的值
    //如果符合条件callBack放行即为通过
    //如果不符合条件callBack传入错误信息即为不通过
    if (/^\d{5,10}$/.test(value)) {
        callback()
    } else {
        callback(new Error('账号必须是5-10位的数字'))
    }
}
const rules = {
    username: [
        {
            trigger: 'change',
            validator: validatorUserName
        }
    ],
    ...
}
</script>

Layout模块

  1. 新建src/layout/index.vue
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
<template>
    <div class="layout_container">
        <!-- 左侧菜单 -->
        <div class="layout_slider" :class="{ fold: LayoutSettingStore.fold ? true : false }">
            <Logo />
            <!-- 展示菜单 -->
            <!-- 滚动组件 -->
            <el-scrollbar class="scollbar">
                <!-- 菜单组件 -->
                <el-menu :collapse="LayoutSettingStore.fold ? true : false" :default-active="$route.path"
                    background-color="#001529" text-color="white" active-text-color="yellowgreen">
                    <!-- 根据路由动态生成菜单 -->
                    <Menu :menuList="userStore.menuRoutes"></Menu>
                </el-menu>
            </el-scrollbar>
        </div>
        <!-- 顶部导航 -->
        <div class="layout_tabbar" :class="{ fold: LayoutSettingStore.fold ? true : false }">
            <!-- layout组件的顶部导航tabbar -->
            <Tabbar />
        </div>
        <!-- 内容展示区 -->
        <div class="layout_main" :class="{ fold: LayoutSettingStore.fold ? true : false }">
            <!-- <p style="height:10000px;background-color:red;">我是一个段落</p> -->
            <Main />
        </div>

    </div>
</template>

<script setup lang="ts">
//引入左侧菜单logo子组件
import Logo from './logo/index.vue'
//引入菜单组件
import Menu from './menu/index.vue'
//右侧内容展示区
import Main from './main/index.vue'
//获取路由对象
import { useRoute } from 'vue-router'
//获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
//引入顶部导航组件tabbar
import Tabbar from './tabbar/index.vue'
import useLayOutSettingStore from '@/store/modules/setting';
//获取layout配置仓库
let LayoutSettingStore = useLayOutSettingStore()

let userStore = useUserStore();
//获取路由对象
let $router = useRoute()
console.log($router.path)
</script>

<script lang="ts">
export default {
    name: 'Layout'
}
</script>

<style scoped lang="scss">
.layout_container {
    width: 100%;
    height: 100vh;

    .layout_slider {
        color: white;
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-bg;
        transition: all 0.3s;

        .scollbar {
            color: white;
            width: 100%;
            height: calc(100vh - #{$base-menu-logo-height});

            .el-menu {
                border-right: none;
            }
        }

        &.fold {
            width: $base-menu-min-width;
        }
    }

    .layout_tabbar {
        position: fixed;
        width: calc(100% - #{$base-menu-width});
        height: $base-tabbar-height;
        top: 0px;
        left: $base-menu-width;
        transition: all 0.3s;

        &.fold {
            width: calc(100vw - $base-menu-min-width);
            left: $base-menu-min-width;
        }
    }

    .layout_main {
        position: absolute;
        width: calc(100% - #{$base-menu-width});
        height: calc(100vh - #{$base-tabbar-height});
        background: yellowgreen;
        top: $base-tabbar-height;
        left: $base-menu-width;
        padding: 20px;
        overflow: auto;
        transition: all 0.3s;

        &.fold {
            width: calc(100vw - $base-menu-min-width);
            left: $base-menu-min-width;
        }
    }
}</style>
  1. /src/styles/variable.scss添加全局变量变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//为项目提供scss全局变量

//定义项目主题颜色
$base-color: purple;

//左侧的菜单的宽度
$base-menu-width: 260px;
$base-menu-min-width: 50px;
//左侧菜单的背景色
$base-menu-bg: #001529;

//顶部导航的高度
$base-tabbar-height: 50px;

//左侧菜单logo高度
$base-menu-logo-height: 50px;

//左侧菜单logo右侧文字大小
$base-logo-title-fontSize: 20px
  1. src/styles/index.scss添加全局的样式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//引入清楚默认样式
@import './reset.scss';

//滚动条外观设置
::-webkit-scrollbar {
  width: 10px;
}

::-webkit-scrollbar-track {
  background: $base-menu-bg;
}

::-webkit-scrollbar-thumb {
  width: 10px;
  background: yellowgreen;
  border-radius: 10px;
}
  1. 新建src/layout/logo/index.vuelogo拆分为一个子组件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
    <div class="logo" v-if="setting.logoHidden">
        <img :src="setting.logo" alt="">
        <p>{{ setting.title }}</p>
    </div>
</template>

<script setup lang="ts">
//引入设置标题和Logo的配置文件
import setting from '@/setting';
</script>
<script lang="ts">
export default {
    name: 'Logo'
}
</script>
<style scoped lang="scss">
.logo {
    width: 100%;
    height: $base-menu-logo-height;
    color: white;
    display: flex;
    align-items: center;
    padding: 10px;

    img {
        width: 40px;
        height: 40px;
    }

    p {
        font-size: $base-logo-title-fontSize;
        margin-left: 50px;
    }
}
</style>
  1. 新建src/setting.ts:用于项目logo和标题配置
1
2
3
4
5
6
//用于项目logo和标题的配置
export default {
    title: 'XXXXXX系统',//项目的标题
    logo: '/public/logo.png',//项目的logo 
    logoHidden: true,//是否隐藏logo
}
  1. 新建src/layout/menu/index.vue:封装动态菜单项,根据项目路由生成
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
    <template v-for="(item, index) in menuList" :key="item.path">
        <!-- 没有子路由 -->
        <template v-if="!item.children">
            <el-menu-item :index="item.path" v-if="!item.meta.hidden" @click="goRoute">
                <el-icon>
                    <component :is="item.meta.icon"></component>
                </el-icon>
                <template #title>
                    <span>{{ item.meta.title }}</span>
                </template>
            </el-menu-item>
        </template>
        <!-- 有子路由但是只有一个子路由 -->
        <template v-if="item.children && item.children.length == 1">
            <el-menu-item :index="item.children[0].path" v-if="!item.children[0].meta.hidden" @click="goRoute">
                <el-icon>
                    <component :is="item.children[0].meta.icon"></component>
                </el-icon>
                <template #title>
                    <span>{{ item.children[0].meta.title }}</span>
                </template>
            </el-menu-item>
        </template>
        <!-- 有子路由且个数大于一个 -->
        <el-sub-menu :index="item.path" v-if="item.children && item.children.length > 1">
            <template #title>
                <el-icon>
                    <component :is="item.meta.icon"></component>
                </el-icon>
                <span>{{ item.meta.title }}</span>
            </template>
            <Menu :menuList="item.children"></Menu>
        </el-sub-menu>
    </template>
</template>

<script setup lang="ts">
//获取父组件传递过来的全部路由数组
defineProps(['menuList'])
import { useRouter } from 'vue-router'
//获取路由器对象
let $router = useRouter()
//点击菜单的回调
const goRoute = (vc: any) => {
    //跳转路由
    $router.push(vc.index)
}
</script>
<script lang="ts">
export default {
    name: 'Menu'
}
</script>
<style scoped></style>
  1. 将路由放到仓库/src/store/modules/user.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//引入路由(常量路由)
import { constantRoutes } from '@/router/routers'
//创建用户小仓库
let useUserStore = defineStore(
	...,
    state: (): UserState => {
        return {
            token: GET_TOKEN(),//用户唯一标识
            menuRoutes: constantRoutes,//用来生成用户菜单路由(数组)
        }
    },
    ...
})
  1. 定义数据类型src/store/modules/types/type.ts
1
2
3
4
5
6
import { RouteRecordRaw } from "vue-router";
//定义小仓库数据state类型
export interface UserState {
    token: string | null;
    menuRoutes: RouteRecordRaw[]
}
  1. src/components/index.ts引入element-plusicon图标组件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//引入element-plus提供的全部图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
export default {
    install(app: App) {
		...;
        //注册element-plus提供的全部图标组件
        for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
            app.component(key, component)
        }
    }
}
  1. 修改路由/src/router/routers.ts:登录后跳转到刚才新建的一级路由页面;并根据路由动态生成菜单项;展示路由元信息
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//对外暴露配置路由(常量路由)
export const constantRoutes = [
    {
        //登录
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login',//命名路由,后面做权限管理时用到
        meta: {
            title: '登录',//菜单标题
            hidden: true,//是否隐藏菜单 true隐藏 false显示
            icon: "promotion"//菜单图标 支持element-plus提供的图标组件
        }
    }
    ,
    {
        //登录成功以后展示数据的路由
        path: '/',
        component: () => import('@/layout/index.vue'),
        name: 'layout',//命名路由,后面做权限管理时用到
        meta: {
            title: '',//菜单标题
            hidden: false,
            icon: ''
        },
        redirect: '/home',
        children: [
            {
                path: '/home',
                component: () => import('@/views/home/index.vue'),
                meta: {
                    title: '首页',//菜单标题
                    hidden: false,
                    icon: 'HomeFilled'
                },
            }
        ]
    }
    ,
    {
        //404
        path: '/404',
        component: () => import('@/views/404/index.vue'),
        name: '404',//命名路由,后面做权限管理时用到
        meta: {
            title: '404',//菜单标题
            hidden: true
        },

    }
    ,
    {
        path: '/screen',
        component: () => import('@/views/screen/index.vue'),
        name: 'Screen',
        meta: {
            title: '数据大屏',//菜单标题
            hidden: false,
            icon: 'Monitor'
        }
    },
    {
        path: '/acl',
        component: () => import('@/layout/index.vue'),
        name: 'Acl',
        meta: {
            title: '权限管理',//菜单标题
            hidden: false,
            icon: 'Lock'
        },
        redirect: '/acl/user',
        children: [
            {
                path: '/acl/user',
                component: () => import('@/views/acl/user/index.vue'),
                name: 'User',
                meta: {
                    title: '用户管理',//菜单标题
                    hidden: false,
                    icon: 'User'
                }
            },
            {
                path: '/acl/role',
                component: () => import('@/views/acl/role/index.vue'),
                name: 'Role',
                meta: {
                    title: '角色管理',//菜单标题
                    hidden: false,
                    icon: 'UserFilled'
                }
            },
            {
                path: '/acl/permission',
                component: () => import('@/views/acl/permission/index.vue'),
                name: 'permission',
                meta: {
                    title: '菜单管理',//菜单标题
                    hidden: false,
                    icon: 'Tools'
                }
            }
        ]
    },
    {
        path: '/product',
        component: () => import('@/layout/index.vue'),
        name: 'Product',
        meta: {
            title: '商品管理',//菜单标题
            hidden: false,
            icon: 'Shop'
        },
        redirect: '/product/trademark',
        children: [
            {
                path: '/product/trademark',
                component: () => import('@/views/product/trademark/index.vue'),
                name: 'Trademark',
                meta: {
                    title: '品牌管理',//菜单标题
                    hidden: false,
                    icon: 'ShoppingCartFull'
                }
            },
            {
                path: '/product/attr',
                component: () => import('@/views/product/attr/index.vue'),
                name: 'Attr',
                meta: {
                    title: '属性管理',//菜单标题
                    hidden: false,
                    icon: 'ChromeFilled'
                }
            },
            {
                path: '/product/spu',
                component: () => import('@/views/product/spu/index.vue'),
                name: 'Spu',
                meta: {
                    title: 'SPU管理',//菜单标题
                    hidden: false,
                    icon: 'Calendar'
                }
            },
            {
                path: '/product/sku',
                component: () => import('@/views/product/sku/index.vue'),
                name: 'Sku',
                meta: {
                    title: 'SKU管理',//菜单标题
                    hidden: false,
                    icon: 'Orange'
                }
            }
        ]
    },
    {
        //任意路由
        path: '/:pathMatch(.*)*',
        redirect: '/404',
        name: 'Any',
        meta: {
            title: '任意路由',//菜单标题
            hidden: true
        },

    }
]
  1. 新建src/views/screen/index.vue数据大屏组件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<template>
    <div>
        <h1>我是 数据大屏一级路由组件</h1>
    </div>
</template>

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

<style scoped></style>
  1. 新建src/views/acl权限管理组件,其下有三个二级路由,新建三个文件user/index.vuerole/index.vuepermission/index.vue,模板如下
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
    <div>
        <h1>菜单管理</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped></style>
<template>
    <div>
        <h1>角色管理</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped></style>

<template>
    <div class="box">
        <h1>用户管理</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped lang="scss">
.box {
    width: 100%;
    height: 400px;
    background-color: rgb(216, 101, 216);
}
</style>
  1. 新建src/views/product产品管理组件,其下有attr/index.vuesku/index.vuespu/index.vuetrademark/index.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
    <div>
        <h1>属性管理</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped></style>
  1. 新建src/layout/main/index.vue,封装右侧内容展示区。可以添加过渡动画
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
    <!-- 路由组件出口 -->
    <router-view v-slot="{ Component }">
        <transition name="fade">
            <!-- 渲染layout一级路由组件的子路由 -->
            <component :is="Component" v-if="flag" />
        </transition>
    </router-view>
</template>

<script setup lang="ts">
import { watch, ref, nextTick } from "vue";
import useLayOutSettingStore from '@/store/modules/setting';
let layoutSettingStore = useLayOutSettingStore()
//控制当前组件是否销毁重建
let flag = ref(true)
//监听仓库内部数据是否发生变化,如果发生变化,说明用户点击过刷新按钮
watch(() => layoutSettingStore.refresh, () => {
    //点击刷新按钮,路由组件销毁
    flag.value = false
    nextTick(() => {
        //路由组件销毁后,再次渲染路由组件
        flag.value = true
    })
})

</script>
<script lang="ts">
export default {
    name: 'Main'
}
</script>
<style scoped lang="scss">
.fade-enter-from {
    opacity: 0;
    transform: scale(0)
}

.fade-enter-active {
    transition: all .3s;
}

.fade-enter-to {
    opacity: 1;
    transform: scale(1)
}
</style>
  1. 新建顶部组件src/layout/tabbar/index.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
    <div class="tabbar">
        <div class="tabbar_left">
            <Breadcrumb />
        </div>
        <div class="tabbar_right">
            <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 {
    name: 'Tabbar'
}
</script>
<style scoped lang="scss">
.tabbar {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: space-between;
    background-image: linear-gradient(to right, rgb(232, 223, 223), rgb(201, 178, 178), rgb(197, 165, 165));

    .tabbar_left {
        display: flex;
        align-items: center;
        margin-left: 20px;
    }

    .tabbar_right {
        display: flex;
        align-items: center;
    }
}
</style>
  1. 新建src/layout/tabbar/breadcrumb/index.vuesrc/layout/tabbar/setting/index.vue:分割顶部组件的左右区域组件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
    <!-- 顶部左侧静态 -->
    <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 { ref } from 'vue'
import { useRoute } from 'vue-router'
import useLayOutSettingStore from '@/store/modules/setting'
//获取layout配置相关的仓库
let LayoutSettingStore = useLayOutSettingStore()
//获取路由对象
let $router = useRoute()
//点击图标的方法
const changeIcon = () => {
    //图标进行切换
    LayoutSettingStore.fold = !LayoutSettingStore.fold
}
const handler = () => {
    console.log($router.matched)
}

</script>
<script lang="ts">
export default {
    name: 'Breadcrumb'
}
</script>
<style scoped></style>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
    <el-button size="small" icon="Refresh" circle=true @click="updateRefresh"></el-button>
    <el-button size="small" icon="FullScreen" circle=true @click="fullScreen"></el-button>
    <el-button size="small" icon="Setting" circle=true></el-button>
    <img src="../../../../public/logo.png" style="width:24px;height:24px;margin:0px 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>退出登录</el-dropdown-item>
            </el-dropdown-menu>
        </template>
    </el-dropdown>
</template>

<script setup lang="ts">
//获取小仓库
import useLayOutSettingStore from '@/store/modules/setting';
let LayoutSettingStore = useLayOutSettingStore()
//刷新按钮点击回调
const updateRefresh = () => {
    //刷新当前路由
    LayoutSettingStore.refresh = !LayoutSettingStore.refresh
}
const fullScreen = () => {
    //DOM对象的一个属性:可以用来当前是否为全屏【全屏:true】
    let full = document.fullscreenElement
    if (!full) {
        //切换为全屏模式
        document.documentElement.requestFullscreen()
    } else {
        //退出全屏模式
        document.exitFullscreen()
    }
}
</script>


<script lang="ts">
export default {
    name: 'Setting'
}
</script>
<style scoped></style>
  1. 新建仓库src/store/modules/setting.ts:保存折叠变量;刷新变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia';

let useLayOutSettingStore = defineStore('SettingStore', {
    state: () => {
        return {
            fold: false,//是否折叠菜单
            refresh: false,//是否刷新页面
        }
    }
})
export default useLayOutSettingStore;

完善部分功能

登陆获取用户信息

  1. 修改src/store/modules/user.ts:获取用户信息后存储到pinia仓库
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//引入接口
import { reqLogin, reqUserInfo } from '@/api/user'
    //小仓库存储数据的地方
    state: (): UserState => {
        return {
            token: GET_TOKEN(),//用户唯一标识
            menuRoutes: constantRoutes,//用来生成用户菜单路由(数组)
            username: '',
            avatar: '',
        }
    },
    //处理异步和逻辑的地方
    actions: {
        //用户登录的方法
        async login(data: LoginParams) {
            //登录请求
            let result: LoginResultModel = await reqLogin(data)
            //登录请求:成功200->token
            //登录请求:失败201->message
            if (result.code === 200) {
                //登录成功
                //pinia仓库存储一下token
                //由于pinia存储数据利用的是js对象,所以未持久化
                this.token = (result.data.token as string)
                //本地存储持久化token
                SET_TOKEN((result.data.token as string))
                //保证当前async函数返回值是一个成功的promise
                return 'ok'
            } else {
                //登录失败
                return Promise.reject(new Error(result.data.message))
            }

        },
        //获取用户信息方法
        async getUserInfo() {
            //获取用户信息存储到仓库中
            let result = await reqUserInfo()
            //如果获取用户信息成功,则存储
            if (result.code == 200) {
                this.username = result.data.checkUser.username
                this.avatar = result.data.checkUser.avatar
            } else {
				return Promise.reject(new Error(result.message))
            }
        }
    },     
  1. 修改src/utils/request.ts:请求拦截器在发送请求时从仓库获取token放入请求头
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//引入用户相关的仓库
import useUserStore from '@/store/modules/user'
request.interceptors.request.use((config) => {
    //获取用户相关的小仓库:获取token
    let userStore = useUserStore();
    if (userStore.token) {
        config.headers.token = userStore.token
    }
    //config是请求配置对象,headers是请求头对象,经常给服务器端传递token
    // config.headers.token='123456'
    //返回配置对象
    return config
})
  1. 新增数据类型src/store/modules/types/type.ts:用户名和头像地址
1
2
3
4
5
6
export interface UserState {
    token: string | null;
    menuRoutes: RouteRecordRaw[],
    username: string,
    avatar: string
}
  1. 修改src/layout/tabbar/setting/index.vue:显示用户名和头像
1
2
3
4
5
6
7
8
9
<img :src="userStore.avatar" style="width:24px;height:24px;margin:0px 10px; border-radius:50%;">
        <span class="el-dropdown-link">
            {{ userStore.username }}
            <el-icon class="el-icon--right">
                <arrow-down />
            </el-icon>
        </span>
import useUserStore from '@/store/modules/user';
let userStore = useUserStore()
  1. src/views/home/index.vue中测试使用仓库中存储的用户名
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<template>
    <div>
        <h1>我是一级路由展示登录成功以后的数据{{ UserStore.username }}</h1>
    </div>
</template>

<script setup lang="ts">
//引入组合式API函数之生命周期函数
import { onMounted } from 'vue'
//获取仓库
import useUserStore from '@/store/modules/user'
let UserStore = useUserStore()
//目前首页挂载完毕发请求获取用户信息
onMounted(() => {
    UserStore.getUserInfo()
})
</script>

<style scoped></style>

退出登录

  1. 修改src/layout/tabbar/setting/index.vue:给退出绑定事件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
<script setup lang="ts">
    import { useRouter } from 'vue-router';
    //获取路由对象
	let $router = useRouter()
	//退出登录点击回调
	const logout = () => {
    //1.向服务器发送请求【退出登录接口,服务器将TOKEN设置为无效】
    //2.清空仓库的TOKEN
    //3.跳转到登陆页面
    userStore.userLogout()
    $router.push({ path: '/login' })
}
</script>
  1. src/store/modules/user.ts添加退出登录的方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/storage'
		//退出登录
        async userLogout() {
            //清空PINIA仓库
            this.token = ''
            this.username = ''
            this.avatar = ''
            REMOVE_TOKEN()
        },
  1. src/utils/storage.ts封装删除TOKEN的方法
1
2
3
4
//本地存储删除数据方法
export const REMOVE_TOKEN = () => {
    localStorage.removeItem('TOKEN')
}

路由鉴权

  1. 安装进度条插件
1
pnpm i nprogress
  1. src下新建permission.ts:利用全局前置守卫和全局后置守卫完成路由鉴权
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//路由鉴权
//鉴权:某一个路由什么条件下才能访问
import router from '@/router'
import setting from './setting'
import nprogress from 'nprogress'  //引入进度条
//关闭进度条加载的右侧小圈
nprogress.configure({ showSpinner: false })
//获取仓库中的token
import useUserStore from './store/modules/user'
import pinia from './store'
let userStore = useUserStore(pinia)  //获取仓库中的token
//引入进度条样式
import 'nprogress/nprogress.css'

//全局前置守卫:项目中任何一个路由发生改变之前,都会先经过这里
router.beforeEach(async (to: any, from: any, next: any) => {
    document.title = `${setting.title} - ${to.meta.title}`
    //to:即将要进入的目标路由对象
    //from:当前导航正要离开的路由
    //next:调用该方法后,才能进入下一个钩子
    nprogress.start();  //开启进度条
    //获取仓库中的token
    let token = userStore.token
    //获取用户名
    let username = userStore.username
    //用户登录判断
    if (token) {  //如果有token
        //登陆成功,访问登录页,直接跳转到首页
        if (to.path == '/login') {
            next({ path: '/' })
        } else {
            //登陆成功访问其余的路由,放行
            //有用户信息
            if (username) {
                next()
            } else {
                //没有用户信息,发请求获取用户信息再放行
                try {
                    //发请求获取用户信息
                    await userStore.getUserInfo()
                    next()
                } catch (error) {//请求失败
                    //token失效,重新登录
                    //用户手动修改了本地存储的token
                    userStore.userLogout()
                    next({ path: '/login' })
                }

            }
        }
    } else {
        //用户未登录判断
        if (to.path === '/login') {  //如果访问的是登录页
            next()  //放行
        } else {//访问的不是登录页
            next({ path: '/login' })  //跳转到登录页
        }
    }
})

//全局后置首位
router.afterEach((to: any, from: any) => {
    nprogress.done()  //关闭进度条
})  //不需要next(),因为已经跳转了

//根据是否有TOKEN判断是否登录
//登陆后可以访问除login外所有路由
//未登录只能访问login,访问其他路由就跳转到login
  1. src/main.ts引入permission.ts
1
2
//引入路由鉴权文件
import './permission';
  1. 修改src/store/modules/user.ts
1
2
3
4
5
6
7
            if (result.code == 200) {
                this.username = result.data.checkUser.username
                this.avatar = result.data.checkUser.avatar
                return 'ok'
            } else {
                return Promise.reject('获取用户信息失败')
            }
  1. 删除views/home/index.vue中发送请求用户信息的代码,因为已经在路由前置守卫中实现了获取用户信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
    <div>
        <h1>我是一级路由展示登录成功以后的数据</h1>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped></style>
  1. 修改src/layout/index.vue的过渡动画,删除左侧菜单的过渡动画
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 滚动组件 -->
<el-scrollbar class="scollbar">
.layout_container {
    width: 100%;
    height: 100vh;

    .layout_slider {
        color: white;
        width: $base-menu-width;
        height: 100vh;
        background: $base-menu-bg;
        transition: all 0.3s;

        .scollbar {
            color: white;
            width: 100%;
            height: calc(100vh - #{$base-menu-logo-height});

            .el-menu {
                border-right: none;
            }
        }
    }

真实接口替代mock接口

  1. 修改三个.env文件:配置服务器地址
1
VITE_SERVE="http://sph-api.atguigu.cn"
  1. vite.config.ts配置代理跨域
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { defineConfig,loadEnv } from 'vite'
export default defineConfig(({ command,mode }) => {
    //获取各种运行环境下的变量(dev/prod/test)
    let env = loadEnv(mode,process.cwd())
    return {
        ...,
    	//代理跨域
        server: {
      		proxy: {
        		[env.VITE_APP_BASE_API]: {
          		//服务器域名
          		target: env.VITE_SERVE,
          		//是否需要代理跨域
          		changeOrigin: true,
          		//路径重写:由于服务器接口不含/api,所以将/api替换为空
          		rewrite: (path) => path.replace(/^\/api/, '')
        		}
      		}
    	}
  	}
})
  1. 重写src/api/user/type.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//登录接口需要携带参数ts类型
export interface LoginFormDate {
    username: string,
    password: string
}

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

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

//定义获取用户信息返回的数据类型
export interface UserInfoResponseData extends ResponseData {
    data: {
        routers: string[],
        buttons: string[],
        roles: string[],
        name: string,
        avatar: string,
    }
}
  1. 重写src/api/user/index.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//统一管理项目用户相关的接口
import request from '@/utils/request'
import type { LoginFormDate, LoginResponseData, UserInfoResponseData } from './type'
//统一管理接口
enum API {
    LOGIN_URL = '/admin/acl/index/login',//登录接口
    USERINFO_URL = '/admin/acl/index/info',//获取用户信息接口
    LOGOUT_URL = '/admin/acl/index/logout',//退出登录接口
}
//对外暴露请求函数
//登录接口方法
export const reqLogin = (data: LoginFormDate) => request.post<any, LoginResponseData>(API.LOGIN_URL, data)
//获取用户信息接口方法
export const reqUserInfo = () => request.get<any, UserInfoResponseData>(API.USERINFO_URL)
//退出登录接口方法
export const reqLogout = () => request.post<any, any>(API.LOGOUT_URL)
  1. 修改src/store/modules/user.ts的用户登录、获取用户信息和退出登录的方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//引入接口
import { reqLogin, reqUserInfo,reqLogout } from '@/api/user'
//引入数据类型
import type { LoginFormDate, LoginResponseData, UserInfoResponseData } from '@/api/type'
//创建用户小仓库
let useUserStore = defineStore(
    //小仓库的名字
    'User', {
    //小仓库存储数据的地方
    state: (): UserState => {
        return {
            token: GET_TOKEN(),//用户唯一标识
            menuRoutes: constantRoutes,//用来生成用户菜单路由(数组)
            username: '',
            avatar: '',
        }
    },
    //处理异步和逻辑的地方
    actions: {
        //用户登录的方法
        async login(data: LoginFormDate) {
            //登录请求
            let result: LoginResponseData = await reqLogin(data)
            //登录请求:成功200->token
            //登录请求:失败201->message
            if (result.code === 200) {
                //登录成功
                //pinia仓库存储一下token
                //由于pinia存储数据利用的是js对象,所以未持久化
                this.token = (result.data as string)
                //本地存储持久化token
                SET_TOKEN((result.data as string))
                //保证当前async函数返回值是一个成功的promise
                return 'ok'
            } else {
                //登录失败
                return Promise.reject(new Error(result.data))
            }

        },
        //获取用户信息方法
        async getUserInfo() {
            //获取用户信息存储到仓库中
            let result: UserInfoResponseData = await reqUserInfo()
            console.log(result)
            //如果获取用户信息成功,则存储
            if (result.code == 200) {
                this.username = result.data.name
                this.avatar = result.data.avatar
                return 'ok'
            } else {
                return Promise.reject(new Error(result.message))
            }
        },
        //退出登录
        async userLogout() {
            //退出登录请求
            let result: any = await reqLogout()
            if (result.code == 200) {
                //清空PINIA仓库
                this.token = ''
                this.username = ''
                this.avatar = ''
                REMOVE_TOKEN()
                return 'ok'
            } else {
                //退出登录的接口请求失败
                return Promise.reject(new Error(result.message))
            }

        },
    },
    getters: {

    }
})
  1. src/views/login/index.vue修改默认密码
1
2
3
4
5
6
//收集账号与密码数据
let loginFrom = reactive({
    //默认值
    username: 'admin',
    password: 'atguigu123'
})
  1. src/layout/tabbar/setting/index.vue添加等待退出登录成功的代码
1
2
3
4
5
6
7
const logout = async () => {
    //1.向服务器发送请求【退出登录接口,服务器将TOKEN设置为无效】
    //2.清空仓库的TOKEN
    //3.跳转到登陆页面
    await userStore.userLogout()
    $router.push({ path: '/login' })
}
  1. src/permission.ts中也添加等待退出登录成功代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try {
    //发请求获取用户信息
    await userStore.getUserInfo()
    next()
} catch (error) {//请求失败
    //token失效,重新登录
    //用户手动修改了本地存储的token
    await userStore.userLogout()
    next({ path: '/login' })
}

首页模块

  1. 修改src/views/home/index.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
    <el-card>
        <div class="box">
            <img :src="userStore.avatar" alt="" class="avatar">
            <div class="bottom">
                <h3 class="title">{{ getTime() }}~{{ userStore.username }}</h3>
                <p class="subtitle">{{ setting.title }}</p>
            </div>
        </div>
    </el-card>
    <div class="bottoms">
        <svg-icon name="welcome" width="500px" height="500px"></svg-icon>
    </div>
</template>

<script setup lang="ts">
//引入用户相关仓库
import useUserStore from '@/store/modules/user'
import { getTime } from '@/utils/time'
import setting from '@/setting';
//获取用户仓库实例
let userStore = useUserStore()
</script>

<style scoped>
.box {
    display: flex;

    .avatar {
        width: 100px;
        height: 100px;
        border-radius: 50%;
    }

    .bottom {
        margin-left: 20px;

        .title {
            font-size: 30px;
            font-weight: 900;
            margin-bottom: 30px;
        }

        .subtitle {
            font-style: italic;
            color: rgb(121, 129, 135);
        }
    }
}

.bottoms {
    display: flex;
    justify-content: center;
    margin-top: 10px;
}</style>

品牌管理模块

  1. src/layout/index.vue删除主题模块的背景颜色,使用默认白色
1
2
3
4
 .layout_main {
	...
 	//background: yellowgreen;
 }
  1. 完成src/views/trademark/index.vue
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
<template>
    <el-card class="box-card">
        <!-- 卡片顶部添加品牌按钮 -->
        <el-button type="primary" size="default" icon="Plus" @click="addTradeMark">添加品牌</el-button>
        <!-- 表格组件用于展示已有数据 -->
        <el-table style="margin: 10px 0px;" border :data="trademarkList">
            <el-table-column label="序号" width="80px" align="center" type="index"></el-table-column>
            <!-- <el-table-column label="品牌名称" prop="tmName"></el-table-column> -->
            <!-- el-table-colum默认用div展示数据也可以用插槽展示数据样式可以自定义 -->
            <el-table-column label="品牌名称">
                <template #="{ row, $index }">
                    <pre style="color:hotpink">{{ row.tmName }}</pre>
                </template>
            </el-table-column>
            <el-table-column label="品牌LOGO">
                <template #="{ row, $index }">
                    <img :src="row.logoUrl" alt="无图片" style="width:100px;height: 100px;">
                </template>
            </el-table-column>
            <el-table-column label="品牌操作">
                <template #="{ row, $index }">
                    <el-button type="primary" size="mini" icon="Edit" @click="$event => updateTradeMark(row)">编辑</el-button>
                    <el-popconfirm :title="`您确定要删除${row.tmName}?`" width="230px" icon="Delete"
                        @confirm='removeTradeMark(row.id)'>
                        <template #reference>
                            <el-button type="danger" size="mini" icon="Delete">删除</el-button>
                        </template>
                    </el-popconfirm>
                </template>
            </el-table-column>
        </el-table>
        <!-- 分页器组件
            paginaation组件的属性
            1. current-page当前页码
            2. page-size每页显示条数
            3. page-sizes每页显示条数的下拉框选项
            4. background是否为分页按钮添加背景色
            5. layout组件布局子组件名用逗号分隔
            6. total总条数
        -->
        <el-pagination @size-change="sizeChange" @current-change="getTrademarkList" v-model:current-page="pageNum"
            v-model:page-size="pageSize" :page-sizes="[5, 10, 15, 20]" :background="true"
            layout="prev, pager, next, jumper,->, sizes,total " :total="total" />
    </el-card>

    <!-- 对话框组件:添加品牌修改品牌业务时使用 -->
    <!-- v-model:控制对话框显示true与隐藏false
    title:设置对话框左上角的标题 -->
    <el-dialog v-model="dialogTableVisible" :title="trademarkForm.id ? '修改品牌' : '添加品牌'">
        <el-form style="width:80%" :model="trademarkForm" :rules="rules" ref="formRef">
            <el-form-item label="品牌名称" label-width="100px" prop="tmName">
                <el-input placeholder="请输入品牌名称" v-model="trademarkForm.tmName"></el-input>
            </el-form-item>
            <el-form-item label="品牌LOGO" label-width="100px" prop="logoUrl">
                <!-- upload相关属性: action:上传的API地址,不带/api代理服务器不工作-->
                <el-upload class="avatar-uploader" action="/api/admin/product/baseTrademark/save" :show-file-list="false"
                    :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
                    <img v-if="trademarkForm.logoUrl" :src="trademarkForm.logoUrl" class="avatar" />
                    <el-icon v-else class="avatar-uploader-icon">
                        <Plus />
                    </el-icon>
                </el-upload>
            </el-form-item>
        </el-form>
        <!-- 具名插槽footer -->
        <template #footer>
            <el-button @click="cancel">取 消</el-button>
            <el-button type="primary" @click="confirm">确 定</el-button>
        </template>
    </el-dialog>
</template>

<script setup lang="ts">
//引入组合式API函数ref
import { ref, onMounted, reactive } from 'vue'
import { reqGetTrademarkList, reqAddOrUpdateTrademark, reqDeleteTrademark } from '@/api/product/trademark'
import type { Records, TrademarkRespnseData, Trademark } from '@/api/product/trademark/types'
import { ElMessage, type UploadProps } from 'element-plus'
import { async } from 'fast-glob';

//当前页码
let pageNum = ref<number>(1)
//每页展示多少条数据
let pageSize = ref<number>(5)
//存储已有品牌数据总数
let total = ref<number>(0)
//存储已有品牌的数据
let trademarkList = ref<Records>([])
//控制对话框显示与隐藏
let dialogTableVisible = ref<boolean>(false)
//收集新增品牌数据
let trademarkForm = reactive<Trademark>({
    tmName: '',
    logoUrl: ''
})
//获取el-form组件实例
let formRef = ref()

//将获取已有品牌数据的接口封装为一个函数:在任何情况下都可以调用
const getTrademarkList = async (pager = 1) => {
    //当每次调用获取品牌数据的函数时,将pageNum变量的值重置为1
    //当切换页码时,传入参数会替换掉默认值1
    pageNum.value = pager
    //调用接口
    let res: TrademarkRespnseData = await reqGetTrademarkList(pageNum.value, pageSize.value)
    if (res.code == 200) {
        //将获取到的数据赋值给total变量
        total.value = res.data.total
        //将获取到的数据赋值给trademarkList变量
        trademarkList.value = res.data.records
    }
}
//组件挂载完成后调用获取品牌数据的函数
onMounted(() => {
    getTrademarkList()
})

//当下拉菜单改变每一页数据量时调用的函数
const sizeChange = () => {
    //调用获取品牌数据的函数(向后端发请求获取数据)
    getTrademarkList()
}

//点击【添加品牌】按钮的回调
const addTradeMark = () => {
    //重置表单数据
    trademarkForm.id = 0
    trademarkForm.tmName = ''
    trademarkForm.logoUrl = ''
    //重置表单校验结果
    formRef.value?.clearValidate('tmName')
    formRef.value?.clearValidate('logoUrl')
    //将对话框显示
    dialogTableVisible.value = true
}

//点击【编辑】按钮的回调
const updateTradeMark = (row: Trademark) => {
    //将获取到的品牌数据赋值给trademarkForm变量
    trademarkForm.id = row.id
    trademarkForm.logoUrl = row.logoUrl
    trademarkForm.tmName = row.tmName
    //重置表单校验结果
    formRef.value?.clearValidate('tmName')
    formRef.value?.clearValidate('logoUrl')
    //将对话框显示
    dialogTableVisible.value = true
}

//点击添加|修改品牌的【取消】按钮的回调
const cancel = () => {
    //将对话框隐藏
    dialogTableVisible.value = false
}

//点击添加|修改品牌的【确定】按钮的回调
const confirm = async () => {
    //在发送请求之前,先进行整个表单的校验
    //如果通过校验才执行下面的代码
    await formRef.value.validate()
    //将对话框隐藏
    dialogTableVisible.value = false
    //调用添加|修改品牌的接口
    let result: any = reqAddOrUpdateTrademark(trademarkForm)
    //判断接口调用是否成功
    if (result.code == 200) {
        //添加|修改成功
        //重新获取品牌数据
        getTrademarkList()
        //提示用户添加成功
        ElMessage.success(trademarkForm.id ? '修改品牌成功' : '添加品牌成功')
    } else {
        //添加|修改失败
        //提示用户添加失败
        ElMessage.error(trademarkForm.id ? '修改品牌失败' : '添加品牌失败')
    }
    //再次发请求获取品牌数据
    getTrademarkList(trademarkForm.id ? pageNum.value : 1)
    // getTrademarkList(pageNum.value)
}

//上传图片之前的钩子函数——约束文件的类型和大小
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
    //rawFile:上传的文件
    //文件类型
    const isJPG = rawFile.type === 'image/jpeg'
    const isPNG = rawFile.type === 'image/png'
    //文件大小
    const isLt2M = rawFile.size / 1024 / 1024 < 2

    if (!isJPG && !isPNG) {
        //提示用户文件类型不符合要求
        ElMessage.error('上传头像图片只能是 JPG/PNG 格式!')
        return false
    }
    if (!isLt2M) {
        //提示用户文件大小不符合要求
        ElMessage.error('上传头像图片大小不能超过 2MB!')
        return false
    }
    return true
}

//上传图片成功的钩子函数
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
    //response:后端返回的数据
    //uploadFile:上传的文件

    if (response.code == 200) {
        //将上传成功的图片地址赋值给logoUrl变量,用于点击【确定】时提交给后端
        trademarkForm.logoUrl = response.data
        //清除图片校验结果
        formRef.value.clearValidate('logoUrl')
        ElMessage.success('上传图片成功')
    } else {
        ElMessage.error('上传图片失败')
    }
}

//品牌自定义校验规则
// const validtorTmName = (rule: any, value: any, callBack: any) => {
//     //rule:当前校验规则
//     //value:用户输入的品牌名称
//     //callBack:回调函数
//     if (value.trim().length >= 2) {
//         callBack()
//     } else {
//         //和下面的message不能并存
//         callBack(new Error('品牌名称不能少于2个字符'))
//     }
// }
//品牌LOGO自定义校验规则
const validtorLogoUrl = (rule: any, value: any, callBack: any) => {
    //value:图片的地址
    if (value) {
        callBack()
    } else {
        callBack(new Error('品牌LOGO务必上传'))
    }
}
//表单验证规则
const rules = {
    tmName: [
        { required: true, message: '品牌名称不能为空', trigger: 'change' }
    ],
    logoUrl: [//没有触发校验的时机
        { required: true, trigger: 'change', validator: validtorLogoUrl }
    ]
}

//删除品牌的气泡确认框的【确认】按钮的回调
const removeTradeMark = async (id: number) => {
    //调用删除品牌的接口
    let result: any = await reqDeleteTrademark(id)
    console.log(result)
    console.log(result.code)
    //判断接口调用是否成功
    if (result.code == 200) {
        //删除成功
        //重新获取品牌数据
        getTrademarkList()
        //提示用户删除成功
        ElMessage.success('删除品牌成功')
        //再次发请求获取品牌数据
        getTrademarkList(trademarkList.value.length > 1 ? pageNum.value : pageNum.value - 1)
    } else {
        //删除失败
        //提示用户删除失败
        ElMessage.error('删除品牌失败')
    }
}
</script>

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

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

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

.el-icon.avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 178px;
    height: 178px;
    text-align: center;
}
</style>
  1. 新建一个文件夹src/api/product/trademark/index.ts封装请求
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//统一管理品牌管理模块接口
import request from "@/utils/request";
import type { TrademarkRespnseData, Trademark } from "./types";

//品牌管理模块接口地址
enum API {
    //获取品牌分页列表的接口地址
    GET_TRADEMARK_LIST = "/admin/product/baseTrademark/",
    //添加品牌
    ADD_TRADEMARK = "/admin/product/baseTrademark/save",
    //修改品牌
    UPDATE_TRADEMARK = "/admin/product/baseTrademark/update",
    //删除品牌
    Delete_TRADEMARK = "/admin/product/baseTrademark/remove/"
}
//获取品牌分页列表的接口方法
//page:获取第几页的数据——默认值为1
//limit:每页显示多少条数据——默认值为10
export const reqGetTrademarkList = (page: number, limit: number) => request.get<any, TrademarkRespnseData>(API.GET_TRADEMARK_LIST + `${page}/${limit}`);
//添加品牌的接口方法
//export const reqAddTrademark = (data: Trademark) => request.post<any,any>(API.ADD_TRADEMARK, data);
//修改品牌的接口方法
//export const reqUpdateTrademark = (data: Trademark) => request.put<any,any>(API.UPDATE_TRADEMARK, data);
//添加与修改已有品牌接口方法
export const reqAddOrUpdateTrademark = (data: Trademark) => {
    //修改已有品牌的数据
    if (data.id) {
        return request.put<any, any>(API.UPDATE_TRADEMARK, data)
    } else {
        //新增品牌
        return request.post<any, any>(API.ADD_TRADEMARK, data)
    }
}
//删除品牌的接口方法
export const reqDeleteTrademark = (id: number) => request.delete<any, any>(API.Delete_TRADEMARK + id);

  1. 配一个types.ts定义数据类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export interface ResponseData {
    code: number;
    message: string;
    ok: boolean;
}
//已有的品牌数据类型
export interface Trademark {
    //?表示可选,可有可无,新增时没有id,修改时有id
    id?: number;
    tmName: string;
    logoUrl: string;
}

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

//获取的已有品牌数据类型
export interface TrademarkRespnseData extends ResponseData {
    data: {
        records: Records;
        total: number;
        size: number;
        pages: number;
        current: number;
        searchCount: boolean;
    }
}

属性管理模块

  1. 新建components/Category/index.vue,抽象出分类组件
1

  1. components/index.ts将分类抽象为全局组件
1
2
3
import Category from './Category/index.vue';
//全局对象
const allGlobalComponents: { [name: string]: Component } = { SvgIcon, Pagination, Category };
  1. 新建src/api/product/attr/index.ts:品牌管理相关API
1

  1. ``views/product/attr/index.vue`:
1

  1. 新建src/store/modules/category.ts:将三级分类的数据存储在仓库中
1

  1. 新建src/api/product/attr/type.ts:添加分类数据的数据类型
1

  1. /store/modules/types/type.ts:定义分类仓库state对象的数据类型
1
2
3
4
5
6
7
8
9
//定义分类仓库state对象的ts类型
export interface CategoryState {
    c1Id: string | number,
    c1Arr: CategoryObj[],
    c2Arr: CategoryObj[],
    c2Id: string | number,
    c3Arr: CategoryObj[],
    c3Id: string | number
}

用户管理模块

  1. src/views/acl/user/index.vue
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
<template>
    <el-card style="height: 80px;">
        <el-form :inline="true" class="form">
            <el-form-item label="用户名:">
                <el-input placeholder="请输入用户名" v-model="keyword"></el-input>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" :disabled="keyword ? false : true" @click="search">查询</el-button>
                <el-button type="primary" @click="reset">重置</el-button>
            </el-form-item>
        </el-form>
    </el-card>
    <el-card style="margin:10px 0px">
        <el-button type="primary" size="default" @click="addUser">添加用户</el-button>
        <el-button type="primary" size="default" :disabled="selectIdArr.length ? false : true"
            @click="deleteSelectUser">批量删除</el-button>
        <!-- table展示用户信息 -->
        <el-table style="margin:10px 0px" border :data="userArr" @selection-change="selectChange">
            <el-table-column type="selection" 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 #="{ row, $index }">
                    <el-button type="primary" size="small" icon="User" @click="setRole(row)">分配角色</el-button>
                    <el-button type="primary" size="small" icon="Edit" @click="updateUser(row)">编辑</el-button>
                    <el-popconfirm :title="`你确定要删除${row.username}?`" width="260px" @confirm="deleteUser(row.id)">
                        <template #reference>
                            <el-button type="primary" size="small" icon="Delete">删除</el-button>
                        </template>
                    </el-popconfirm>
                </template>
            </el-table-column>
        </el-table>
        <!-- 分页器 -->
        <el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[5, 7, 9, 11]"
            :background="true" layout="prev, pager, next, jumper,->, sizes,total" :total="total"
            @current-change="getHasUser" @size-change="handler" />
    </el-card>
    <!-- 抽屉结构:完成添加新的用户账户|更新已有的账号信息 -->
    <el-drawer v-model="drawer">
        <!-- 头部标题:将来文字内容应该是动态的 -->
        <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 @click="cancel">取消</el-button>
                <el-button type="primary" @click="save">确定</el-button>
            </div>
        </template>
    </el-drawer>
    <!-- 抽屉结构:分配角色 -->
    <el-drawer v-model="drawer1">
        <template #header>
            <h4>分配角色</h4>
        </template>
        <template #default>
            <el-form>
                <el-form-item label="用户姓名">
                    <el-input v-model="userParams.username" :disabled="true"></el-input>
                </el-form-item>
                <el-form-item label="角色列表">
                    <el-checkbox v-model="checkAll" :indeterminate="isIndeterminate"
                        @change="handleCheckAllChange">全选</el-checkbox>
                    <!-- 显示角色的复选框 -->
                    <el-checkbox-group v-model="userRole" @change="handleCheckedCitiesChange">
                        <el-checkbox v-for="(role, index) in AllRole" :key="index" :label="role">{{ role.roleName
                        }}</el-checkbox>
                    </el-checkbox-group>
                </el-form-item>
            </el-form>
        </template>
        <template #footer>
            <div style="flex: a uto">
                <el-button @click="drawer1 == false">取消</el-button>
                <el-button type="primary" @click="confirmClick">确定</el-button>
            </div>
        </template>
    </el-drawer>
</template>

<script setup lang="ts">
import { ref, onMounted, reactive, nextTick } from 'vue'
import { reqUserInfo, reqAddOrUpdateUser, reqAllRole, reqSetUserRole, reqRemoveUser, reqSelectUser } from '@/api/acl/user'
import type { UserResponseData, Records, User, AllRoleResponseData, AllRole, SetRoleData } from '@/api/acl/user/type'
import { ElMessage, resultProps } from 'element-plus';
import useLayOutSettingStore from '@/store/modules/setting'
//默认页码
let pageNo = ref<number>(1)
//默认每页条数
let pageSize = ref<number>(5)
//用户总个数
let total = ref<number>(0)
//存储所有用户的数组
let userArr = ref<Records>([])
//定义响应式数据控制抽屉的显示与隐藏
let drawer = ref<boolean>(false)
//收集用户信息的响应式数据
let userParams = reactive({
    username: '',
    name: '',
    password: ''
})
//获取from组件实例
let formRef = ref<any>()
//控制分类角色抽屉的显示与隐藏
let drawer1 = ref<boolean>(false)
//控制全选按钮的选中与否
let checkAll = ref(false)
//控制全选按钮的半选状态
const isIndeterminate = ref(true)
//存储全部职位的数据
let AllRole = ref<AllRole>([])
//当前用户已有的职位
let userRole = ref<AllRole>([])
//准备一个数组存储批量删除的用户的ID
let selectIdArr = ref<User[]>([]);
//定义响应式数据:收集用户输入进来的关键字
let keyword = ref<string>('');
//获取模板setting仓库
let settingStore = useLayOutSettingStore();
//组件挂载完毕
onMounted(() => {
    getHasUser()
})
//获取全部已有的用户信息
const getHasUser = async (pager = 1) => {
    //收集当前页码
    pageNo.value = pager
    //发送请求
    let result: UserResponseData = await reqUserInfo(pageNo.value, pageSize.value, keyword.value);
    if (result.code === 200) {
        //获取用户总个数
        total.value = result.data.total
        //获取用户信息
        userArr.value = result.data.records
    }
}
//分页器下拉菜单的自定义事件的回调
const handler = () => {
    //重新发送请求
    getHasUser()
}
//添加用户按钮的回调
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('password')
    })
}
//更新用户信息按钮回调
//row为当前行的数据
const updateUser = (row: User) => {
    //显示抽屉
    drawer.value = true
    //存储已有的账号信息
    Object.assign(userParams, row)
    //清除上一次的错误提示信息
    nextTick(() => {
        formRef.value.clearValidate('username')
        formRef.value.clearValidate('name')
        formRef.value.clearValidate('password')
    })
}
//保存按钮的回调
const save = async () => {
    //点击保存按钮时,保证表单校验通过再发送请求
    await formRef.value.validate()
    //保存按钮:添加新的用户|更新已有的用户信息
    let result: any = 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 cancel = () => {
    //关闭抽屉
    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) => {

    //存储已有用户信息
    Object.assign(userParams, row)
    //获取全部的职位的数据与当前用户已有的职位的数据
    let result: AllRoleResponseData = await reqAllRole(userParams.id)
    if (result.code == 200) {
        //存储全部的职位
        AllRole.value = result.data.allRolesList
        //存储当前用户已有的职位
        userRole.value = result.data.assignRoles
        //显示抽屉
        drawer1.value = true
    }
}
//全选复选框的change事件
const handleCheckAllChange = (val: boolean) => {
    userRole.value = val ? AllRole.value : []
    isIndeterminate.value = false
}
//底部复选框的change事件
const handleCheckedCitiesChange = (value: string[]) => {
    //已经勾选的项目的长度
    const checkedCount = value.length
    checkAll.value = checkedCount === AllRole.value.length
    //顶部复选框不确定的样式
    isIndeterminate.value = !(checkedCount === AllRole.value.length)
}
//分配角色确定按钮的回调
const confirmClick = async () => {
    //收集参数
    let data: SetRoleData = {
        userId: (userParams.id as number),
        roleIdList: userRole.value.map(item => {
            return (item.id as number)
        })
    }
    //分配用户的职位
    let result: any = await reqSetUserRole(data);
    if (result.code == 200) {
        //提示信息
        ElMessage({ type: 'success', message: '分配职务成功' });
        //关闭抽屉
        drawer1.value = false;
        //获取更新完毕用户的信息,更新完毕留在当前页
        getHasUser(pageNo.value);

    }
}
//删除某一个用户
const deleteUser = async (userId: number) => {
    let result: any = await reqRemoveUser(userId);
    if (result.code == 200) {
        ElMessage({ type: 'success', message: '删除成功' });
        getHasUser(userArr.value.length > 1 ? pageNo.value : pageNo.value - 1);
    }
}
//table复选框勾选的时候会触发的事件
const selectChange = (value: any) => {
    selectIdArr.value = value;
}
//批量删除按钮的回调
const deleteSelectUser = async () => {
    //整理批量删除的参数
    let idsList: any = selectIdArr.value.map(item => {
        return item.id;
    });
    //批量删除的请求
    let result: any = await reqSelectUser(idsList);
    if (result.code == 200) {
        ElMessage({ type: 'success', message: '删除成功' });
        getHasUser(userArr.value.length > 1 ? pageNo.value : pageNo.value - 1);
    }
}
//搜索按钮的回调
const search = () => {
    //根据关键字获取相应的用户数据
    getHasUser();
    //清空关键字
    keyword.value = '';
}
//重置按钮
const reset = () => {
    settingStore.refresh = !settingStore.refresh;
}
</script>

<style scoped lang="scss">
.form {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
</style>
  1. src/api/acl/user/index.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//用户管理模块的接口
import request from "@/utils/request";
import type { UserResponseData, User, AllRoleResponseData, SetRoleData } from "./type";
//枚举地址
enum API {
    //获取全部已有用户账号信息
    ALLUSER_URL = '/admin/acl/user/',
    //添加一个新的用户账号
    ADDUSER_URL = "/admin/acl/user/save",
    //更新已有的用户账号
    UPDATEUSER_URL = '/admin/acl/user/update',
    //获取全部职位,当前账号拥有的职位接口
    ALLROLEURL = '/admin/acl/user/toAssign/',
    //给已有的用户分配角色接口
    SETROLE_URL = '/admin/acl/user/doAssignRole',
    //删除某一个账号
    DELETEUSER_URL = '/admin/acl/user/remove/',
    //批量删除的接口
    DELETEALLUSER_URL = '/admin/acl/user/batchRemove'

}
//获取用户账号信息的接口
export const reqUserInfo = (page: number, limit: number, username: string) => request.get<any, UserResponseData>(API.ALLUSER_URL + `${page}/${limit}/?username=${username}`);
//添加用户与更新已有用户的接口
export const reqAddOrUpdateUser = (data: User) => {
    //携带参数有ID更新
    if (data.id) {
        return request.put<any, any>(API.UPDATEUSER_URL, data);
    } else {
        return request.post<any, any>(API.ADDUSER_URL, data);
    }
}
//获取全部职位以及包含当前用户的已有的职位
export const reqAllRole = (userId: number) => request.get<any, AllRoleResponseData>(API.ALLROLEURL + userId);
//分配职位
export const reqSetUserRole = (data: SetRoleData) => request.post<any, any>(API.SETROLE_URL, data);
//删除某一个账号的信息
export const reqRemoveUser = (userId: number) => request.delete<any, any>(API.DELETEUSER_URL + userId);
//批量删除的接口
export const reqSelectUser = (idList: number[]) => request.delete(API.DELETEALLUSER_URL, { data: idList });
  1. src/api/acl/user/type.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//账号信息的ts类型
export interface ResponseData {
    code: number,
    message: string,
    ok: boolean
}
//代表一个账号信息的ts类型
export interface User {
    id?: number,
    "createTime"?: string,
    "updateTime"?: string,
    "username"?: string,
    "password"?: string,
    "name"?: string,
    "phone"?: null,
    "roleName"?: string
}
//数组包含全部的用户信息
export type Records = User[];
//获取全部用户信息接口返回的数据ts类型
export interface UserResponseData extends ResponseData {
    data: {
        records: Records,
        "total": number,
        "size": number,
        "current": number,
        "pages": number
    }
}

//代表一个职位的ts类型
export interface RoleData {
    "id"?: number,
    "createTime"?: string,
    "updateTime"?: string,
    "roleName": string,
    "remark": null
}
//全部职位的列表
export type AllRole = RoleData[];
//获取全部职位的接口返回的数据ts类型
export interface AllRoleResponseData extends ResponseData {
    data: {
        assignRoles: AllRole,
        allRolesList: AllRole
    }
}

//给用户分配职位接口携带参数的ts类型
export interface SetRoleData {
    "roleIdList": number[],
    "userId": number
}

角色管理模块

  1. src/views/acl/role/index.vue
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
<template>
    <el-card>
        <el-form :inline="true" class="form">
            <el-form-item label="职位搜索">
                <el-input placeholder="请你输入搜索职位关键字" v-model="keyword"></el-input>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" size="default" :disabled="keyword ? false : true" @click="search">搜索</el-button>
                <el-button type="primary" size="default" @click="reset">重置</el-button>
            </el-form-item>
        </el-form>
    </el-card>
    <el-card style="margin: 10px 0px;">
        <el-button type="primary" size="default" icon="Plus" @click="addRole">添加职位</el-button>
        <el-table border style="margin: 10px 0px;" :data="allRole">
            <el-table-column type="index" align="center" label="#"></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" show-overflow-tooltip prop="createTime"></el-table-column>
            <el-table-column label="更新时间" align="center" show-overflow-tooltip prop="updateTime"></el-table-column>
            <el-table-column label="操作" width="280px" align="center">
                <!-- row:已有的职位对象 -->
                <template #="{ row, $index }">
                    <el-button type="primary" size="small" icon="User" @click="setPermisstion(row)">分配权限</el-button>
                    <el-button type="primary" size="small" icon="Edit" @click="updateRole(row)">编辑</el-button>
                    <el-popconfirm :title="`你确定要删除${row.roleName}?`" width="260px" @confirm="removeRole(row.id)">
                        <template #reference>
                            <el-button type="primary" size="small" icon="Delete">删除</el-button>
                        </template>
                    </el-popconfirm>
                </template>
            </el-table-column>
        </el-table>
        <el-pagination v-model:current-page="pageNo" v-model:page-size="pageSize" :page-sizes="[10, 20, 30, 40]"
            :background="true" layout="prev, pager, next, jumper,->,sizes,total" :total="total" @current-change="getHasRole"
            @size-change="sizeChange" />
    </el-card>
    <!-- 添加职位与更新已有职位的结构:对话框 -->
    <el-dialog v-model="dialogVisite" :title="RoleParams.id ? '更新职位' : '添加职位'">
        <el-form :model="RoleParams" :rules="rules" ref="form">
            <el-form-item label="职位名称" prop="roleName">
                <el-input placeholder="请你输入职位名称" v-model="RoleParams.roleName"></el-input>
            </el-form-item>
        </el-form>
        <template #footer>
            <el-button type="primary" size="default" @click="dialogVisite = false">取消</el-button>
            <el-button type="primary" size="default" @click="save">确定</el-button>
        </template>
    </el-dialog>
    <!-- 抽屉组件:分配职位的菜单权限与按钮的权限 -->
    <el-drawer v-model="drawer">
        <template #header>
            <h4>分配菜单与按钮的权限</h4>
        </template>
        <template #default>
            <!-- 树形控件 -->
            <el-tree ref="tree" :data="menuArr" show-checkbox node-key="id" default-expand-all
                :default-checked-keys="selectArr" :props="defaultProps" />
        </template>
        <template #footer>
            <div style="flex: auto">
                <el-button @click="drawer = false">取消</el-button>
                <el-button type="primary" @click="handler">确定</el-button>
            </div>
        </template>
    </el-drawer>
</template>

<script setup lang="ts">
import { ref, onMounted, reactive, nextTick } from 'vue';
//请求方法
import { reqRemoveRole, reqAllRoleList, reqAddOrUpdateRole, reqAllMenuList, reqSetPermisstion } from '@/api/acl/role';
import type { RoleResponseData, Records, RoleData, MenuResponseData, MenuList } from '@/api/acl/role/type'
//引入骨架的仓库
import useLayOutSettingStore from '@/store/modules/setting';
import { ElMessage } from 'element-plus';
let settingStore = useLayOutSettingStore();
//当前页码
let pageNo = ref<number>(1);
//一页展示几条数据
let pageSize = ref<number>(10);
//搜索职位关键字
let keyword = ref<string>('');
//存储全部已有的职位
let allRole = ref<Records>([]);
//职位总个数
let total = ref<number>(0);
//控制对话框的显示与隐藏
let dialogVisite = ref<boolean>(false);
//获取form组件实例
let form = ref<any>();
//控制抽屉显示与隐藏
let drawer = ref<boolean>(false);
//收集新增岗位数据
let RoleParams = reactive<RoleData>({
    roleName: ''
})
//准备一个数组:数组用于存储勾选的节点的ID(四级的)
let selectArr = ref<number[]>([]);
//定义数组存储用户权限的数据
let menuArr = ref<MenuList>([]);
//获取tree组件实例
let tree = ref<any>();
//组件挂载完毕
onMounted(() => {
    //获取职位请求
    getHasRole();
});
//获取全部用户信息的方法|分页器当前页码发生变化的回调
const getHasRole = async (pager = 1) => {
    //修改当前页码
    pageNo.value = pager;
    let result: RoleResponseData = await reqAllRoleList(pageNo.value, pageSize.value, keyword.value);
    if (result.code == 200) {
        total.value = result.data.total;
        allRole.value = result.data.records;
    }
}
//下拉菜单的回调
const sizeChange = () => {
    getHasRole();
}
//搜索按钮的回调
const search = () => {
    //再次发请求根据关键字
    getHasRole();
    keyword.value = '';
}
//重置按钮的回调
const reset = () => {
    settingStore.refsh = !settingStore.refsh;
}
//添加职位按钮的回调
const addRole = () => {
    //对话框显示出来
    dialogVisite.value = true;
    //清空数据
    Object.assign(RoleParams, {
        roleName: '',
        id: 0
    });
    //清空上一次表单校验错误结果
    nextTick(() => {
        form.value.clearValidate('roleName');
    })

}
//更新已有的职位按钮的回调
const updateRole = (row: RoleData) => {
    //显示出对话框
    dialogVisite.value = true;
    //存储已有的职位----带有ID的
    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 save = async () => {
    //表单校验结果,结果通过在发请求、结果没有通过不应该在发生请求
    await form.value.validate();
    //添加职位|更新职位的请求
    let result: any = await reqAddOrUpdateRole(RoleParams);
    if (result.code == 200) {
        //提示文字
        ElMessage({ type: 'success', message: RoleParams.id ? '更新成功' : '添加成功' });
        //对话框显示
        dialogVisite.value = false;
        //再次获取全部的已有的职位
        getHasRole(RoleParams.id ? pageNo.value : 1);
    }
}

//分配权限按钮的回调
//已有的职位的数据
const setPermisstion = async (row: RoleData) => {
    //抽屉显示出来
    drawer.value = true;
    //收集当前要分类权限的职位的数据
    Object.assign(RoleParams, row);
    //根据职位获取权限的数据
    let result: MenuResponseData = await reqAllMenuList((RoleParams.id as number));
    if (result.code == 200) {
        menuArr.value = result.data;
        selectArr.value = filterSelectArr(menuArr.value, []);
    }
}
//树形控件的测试数据
const defaultProps = {
    children: 'children',
    label: 'name',
}

const filterSelectArr = (allData: any, initArr: any) => {
    allData.forEach((item: any) => {
        if (item.select && item.level == 4) {
            initArr.push(item.id);
        }
        if (item.children && item.children.length > 0) {
            filterSelectArr(item.children, initArr);
        }
    })

    return initArr;
}

//抽屉确定按钮的回调
const handler = async () => {
    //职位的ID
    const roleId = (RoleParams.id as number);
    //选中节点的ID
    let arr = tree.value.getCheckedKeys();
    //半选的ID
    let arr1 = tree.value.getHalfCheckedKeys();
    let permissionId = arr.concat(arr1);
    //下发权限
    let result: any = await reqSetPermisstion(roleId, permissionId);
    if (result.code == 200) {
        //抽屉关闭
        drawer.value = false;
        //提示信息
        ElMessage({ type: 'success', message: '分配权限成功' });
        //页面刷新
        window.location.reload();
    }
}

//删除已有的职位
const removeRole = async (id: number) => {
    let result: any = await reqRemoveRole(id);
    if (result.code == 200) {
        //提示信息
        ElMessage({ type: 'success', message: '删除成功' });
        getHasRole(allRole.value.length > 1 ? pageNo.value : pageNo.value - 1);
    }
}
</script>

<style scoped>
.form {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 50px;
}
</style>
  1. src/api/acl/role/index.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//角色管理模块的的接口
import request from "@/utils/request";
import type { RoleResponseData, RoleData, MenuResponseData } from './type'
//枚举地址
enum API {
    //获取全部的职位接口
    ALLROLE_URL = '/admin/acl/role/',
    //新增岗位的接口地址
    ADDROLE_URL = '/admin/acl/role/save',
    //更新已有的职位
    UPDATEROLE_URL = '/admin/acl/role/update',
    //获取全部的菜单与按钮的数据
    ALLPERMISSTION = "/admin/acl/permission/toAssign/",
    //给相应的职位分配权限
    SETPERMISTION_URL = '/admin/acl/permission/doAssign/?',
    //删除已有的职位
    REMOVEROLE_URL = '/admin/acl/role/remove/'
}
//获取全部的角色
export const reqAllRoleList = (page: number, limit: number, roleName: string) => request.get<any, RoleResponseData>(API.ALLROLE_URL + `${page}/${limit}/?roleName=${roleName}`);
//添加职位与更新已有职位接口
export const reqAddOrUpdateRole = (data: RoleData) => {
    if (data.id) {
        return request.put<any, any>(API.UPDATEROLE_URL, data);
    } else {
        return request.post<any, any>(API.ADDROLE_URL, data);
    }
}

//获取全部菜单与按钮权限数据
export const reqAllMenuList = (roleId: number) => request.get<any, MenuResponseData>(API.ALLPERMISSTION + roleId);
//给相应的职位下发权限
export const reqSetPermisstion = (roleId: number, permissionId: number[]) => request.post(API.SETPERMISTION_URL + `roleId=${roleId}&permissionId=${permissionId}`);
//删除已有的职位
export const reqRemoveRole = (roleId: number) => request.delete<any, any>(API.REMOVEROLE_URL + roleId)
  1. src/api/acl/role/type.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
export interface ResponseData {
    code: number,
    message: string,
    ok: boolean
}
//职位数据类型
export interface RoleData {
    "id"?: number,
    "createTime"?: string,
    "updateTime"?: string,
    "roleName": string,
    "remark"?: null
}

//全部职位的数组的ts类型
export type Records = RoleData[];
//全部职位数据的相应的ts类型
export interface RoleResponseData extends ResponseData {
    data: {
        records: Records,
        "total": number,
        "size": number,
        "current": number,
        "orders": [],
        "optimizeCountSql": boolean,
        "hitCount": boolean,
        "countId": null,
        "maxLimit": null,
        "searchCount": boolean,
        "pages": number
    }
}


//菜单与按钮数据的ts类型
export interface MunuData {
    "id": number,
    "createTime": string,
    "updateTime": string,
    "pid": number,
    "name": string,
    "code": string,
    "toCode": string,
    "type": number,
    "status": null,
    "level": number,
    "children"?: MenuList,
    "select": boolean
}
export type MenuList = MunuData[];

//菜单权限与按钮权限数据的ts类型
export interface MenuResponseData extends ResponseData {
    data: MenuList
}

菜单管理模块

  1. src/views/acl/permission/index.vue
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<template>
    <el-table :data="PermisstionArr" style="width: 100%; margin-bottom: 20px" row-key="id" border>
        <el-table-column label="名称" prop="name"></el-table-column>
        <el-table-column label="权限值" prop="code"></el-table-column>
        <el-table-column label="修改时间" prop="updateTime"></el-table-column>
        <el-table-column label="操作">
            <!-- row:即为已有的菜单对象|按钮的对象的数据 -->
            <template #="{ row, $index }">
                <el-button type="primary" @click="addPermisstion(row)" size="small"
                    :disabled="row.level == 4 ? true : false">{{
                        row.level == 3 ? '添加功能'
                        : '添加菜单' }}</el-button>
                <el-button type="primary" @click="updatePermisstion(row)" size="small"
                    :disabled="row.level == 1 ? true : false">编辑</el-button>
                <el-popconfirm :title="`你确定要删除${row.name}?`" width="260px" @confirm="removeMenu(row.id)">
                    <template #reference>
                        <el-button type="primary" size="small" :disabled="row.level == 1 ? true : false">删除</el-button>
                    </template>
                </el-popconfirm>

            </template>
        </el-table-column>
    </el-table>
    <!-- 对话框组件:添加或者更新已有的菜单的数据结构 -->
    <el-dialog v-model="dialogVisible" :title="menuData.id ? '更新菜单' : '添加菜单'">
        <!-- 表单组件:收集新增与已有的菜单的数据 -->
        <el-form>
            <el-form-item label="名称">
                <el-input placeholder="请你输入菜单名称" v-model="menuData.name"></el-input>
            </el-form-item>
            <el-form-item label="权限">
                <el-input placeholder="请你输入权限数值" v-model="menuData.code"></el-input>
            </el-form-item>
        </el-form>
        <template #footer>
            <span class="dialog-footer">
                <el-button @click="dialogVisible = false">取消</el-button>
                <el-button type="primary" @click="save">
                    确定
                </el-button>
            </span>
        </template>
    </el-dialog>
</template>

<script setup lang="ts">
import { ref, onMounted, reactive } from "vue";
//引入获取菜单请求API
import { reqAllPermisstion, reqAddOrUpdateMenu, reqRemoveMenu } from '@/api/acl/menu';
//引入ts类型
import type { MenuParams, PermisstionResponseData, PermisstionList, Permisstion } from '@/api/acl/menu/type';
import { ElMessage } from "element-plus";
//存储菜单的数据
let PermisstionArr = ref<PermisstionList>([]);
//控制对话框的显示与隐藏
let dialogVisible = ref<boolean>(false);
//携带的参数
let menuData = reactive<MenuParams>({
    "code": "",
    "level": 0,
    "name": "",
    "pid": 0,
})
//组件挂载完毕
onMounted(() => {
    getHasPermisstion();
});
//获取菜单数据的方法
const getHasPermisstion = async () => {
    let result: PermisstionResponseData = await reqAllPermisstion();
    if (result.code == 200) {
        PermisstionArr.value = result.data;
    }
}

//添加菜单按钮的回调
const addPermisstion = (row: Permisstion) => {
    //清空数据
    Object.assign(menuData, {
        id: 0,
        "code": "",
        "level": 0,
        "name": "",
        "pid": 0,
    })
    //对话框显示出来
    dialogVisible.value = true;
    //收集新增的菜单的level数值
    menuData.level = row.level + 1;
    //给谁新增子菜单
    menuData.pid = (row.id as number);

}
//编辑已有的菜单
const updatePermisstion = (row: Permisstion) => {
    dialogVisible.value = true;
    //点击修改按钮:收集已有的菜单的数据进行更新
    Object.assign(menuData, row);
}

//确定按钮的回调
const save = async () => {
    //发请求:新增子菜单|更新某一个已有的菜单的数据
    let result: any = await reqAddOrUpdateMenu(menuData);
    if (result.code == 200) {
        //对话框隐藏
        dialogVisible.value = false;
        //提示信息
        ElMessage({ type: 'success', message: menuData.id ? '更新成功' : '添加成功' })
        //再次获取全部最新的菜单的数据
        getHasPermisstion();
    }
}

//删除按钮回调
const removeMenu = async (id: number) => {
    let result = await reqRemoveMenu(id);
    if (result.code == 200) {
        ElMessage({ type: 'success', message: '删除成功' });
        getHasPermisstion();

    }
}


</script>

<style scoped></style>
  1. src/api/acl/menu/index.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import request from "@/utils/request";
import type { PermisstionResponseData, MenuParams } from './type';
//枚举地址
enum API {
    //获取全部菜单与按钮的标识数据
    ALLPERMISSTION_URL = '/admin/acl/permission',
    //给某一级菜单新增一个子菜单
    ADDMENU_URL = '/admin/acl/permission/save',
    //更新某一个已有的菜单
    UPDATE_URL = '/admin/acl/permission/update',
    //删除已有的菜单
    DELETEMENU_URL = '/admin/acl/permission/remove/'
}
//获取菜单数据
export const reqAllPermisstion = () => request.get<any, PermisstionResponseData>(API.ALLPERMISSTION_URL);
//添加与更新菜单的方法
export const reqAddOrUpdateMenu = (data: MenuParams) => {
    if (data.id) {
        return request.put<any, any>(API.UPDATE_URL, data);
    } else {
        return request.post<any, any>(API.ADDMENU_URL, data);
    }
}

//删除某一个已有的菜单
export const reqRemoveMenu = (id: number) => request.delete<any, any>(API.DELETEMENU_URL + id);

  1. src/api/acl/menu/type.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//数据类型定义
export interface ResponseData {
    code: number,
    message: string,
    ok: boolean
}
//菜单数据与按钮数据的ts类型
export interface Permisstion {
    "id"?: number,
    "createTime": string,
    "updateTime": string,
    "pid": number,
    "name": string,
    "code": null,
    "toCode": null,
    "type": number,
    "status": null,
    "level": number,
    "children"?: PermisstionList,
    "select": boolean
}
export type PermisstionList = Permisstion[];
//菜单接口返回的数据类型
export interface PermisstionResponseData extends ResponseData {
    data: PermisstionList
}

//添加与修改菜单携带的参数的ts类型
export interface MenuParams {
    id?: number,//ID
    "code": string,//权限数值
    "level": number,//几级菜单
    "name": string,//菜单的名字
    "pid": number,//菜单的ID
}
  1. src/router/router.ts:将路由拆分为常量路由、异步路由、任意路由
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//对外暴露配置路由(常量路由):全部用户都可以访问到的路由
export const constantRoute = [
    {
        //登录
        path: '/login',
        component: () => import('@/views/login/index.vue'),
        name: 'login',
        meta: {
            title: '登录',//菜单标题
            hidden: true,//代表路由标题在菜单中是否隐藏  true:隐藏 false:不隐藏
            icon: "Promotion",//菜单文字左侧的图标,支持element-plus全部图标
        }
    }
    ,
    {
        //登录成功以后展示数据的路由
        path: '/',
        component: () => import('@/layout/index.vue'),
        name: 'layout',
        meta: {
            title: '',
            hidden: false,
            icon: ''
        },
        redirect: '/home',
        children: [
            {
                path: '/home',
                component: () => import('@/views/home/index.vue'),
                meta: {
                    title: '首页',
                    hidden: false,
                    icon: 'HomeFilled'
                }
            }
        ]
    },
    {
        //404
        path: '/404',
        component: () => import('@/views/404/index.vue'),
        name: '404',
        meta: {
            title: '404',
            hidden: true,
            icon: 'DocumentDelete'
        }
    },
    {
        path: '/screen',
        component: () => import('@/views/screen/index.vue'),
        name: 'Screen',
        meta: {
            hidden: false,
            title: '数据大屏',
            icon: 'Platform'
        }
    }]

//异步路由
export const asnycRoute = [
    {
        path: '/acl',
        component: () => import('@/layout/index.vue'),
        name: 'Acl',
        meta: {
            title: '权限管理',
            icon: 'Lock'
        },
        redirect: '/acl/user',
        children: [
            {
                path: '/acl/user',
                component: () => import('@/views/acl/user/index.vue'),
                name: 'User',
                meta: {
                    title: '用户管理',
                    icon: 'User'
                }
            },
            {
                path: '/acl/role',
                component: () => import('@/views/acl/role/index.vue'),
                name: 'Role',
                meta: {
                    title: '角色管理',
                    icon: 'UserFilled'
                }
            },
            {
                path: '/acl/permission',
                component: () => import('@/views/acl/permission/index.vue'),
                name: 'Permission',
                meta: {
                    title: '菜单管理',
                    icon: 'Monitor'
                }
            }
        ]
    }
    ,
    {
        path: '/product',
        component: () => import('@/layout/index.vue'),
        name: 'Product',
        meta: {
            title: '商品管理',
            icon: 'Goods',
        },
        redirect: '/product/trademark',
        children: [
            {
                path: '/product/trademark',
                component: () => import('@/views/product/trademark/index.vue'),
                name: "Trademark",
                meta: {
                    title: '品牌管理',
                    icon: 'ShoppingCartFull',
                }
            },
            {
                path: '/product/attr',
                component: () => import('@/views/product/attr/index.vue'),
                name: "Attr",
                meta: {
                    title: '属性管理',
                    icon: 'ChromeFilled',
                }
            },
            {
                path: '/product/spu',
                component: () => import('@/views/product/spu/index.vue'),
                name: "Spu",
                meta: {
                    title: 'SPU管理',
                    icon: 'Calendar',
                }
            },
            {
                path: '/product/sku',
                component: () => import('@/views/product/sku/index.vue'),
                name: "Sku",
                meta: {
                    title: 'SKU管理',
                    icon: 'Orange',
                }
            },
        ]
    }
]

//任意路由
export const anyRoute = {
    //任意路由
    path: '/:pathMatch(.*)*',
    redirect: '/404',
    name: 'Any',
    meta: {
        title: '任意路由',
        hidden: true,
        icon: 'DataLine'
    }
}
  1. 安装lodash
1
pnpm i lodash
  1. src/store/modules/user.ts:用户登陆后计算路由和添加按钮权限
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { constantRoute, asnycRoute, anyRoute } from '@/router/routes';
//引入深拷贝方法
//@ts-ignore
import cloneDeep from 'lodash/cloneDeep'
import router from '@/router';
//用于过滤当前用户需要展示的异步路由
function filterAsyncRoute(asnycRoute: any, routes: any) {
    return asnycRoute.filter((item: any) => {
        if (routes.includes(item.name)) {
            if (item.children && item.children.length > 0) {
                //硅谷333账号:product\trademark\attr\sku
                item.children = filterAsyncRoute(item.children, routes);
            }
            return true;
        }
    })
}
    //小仓库存储数据地方
    state: (): UserState => {
        return {
            token: GET_TOKEN(),//用户唯一标识token
            menuRoutes: constantRoute,//仓库存储生成菜单需要数组(路由)
            username: '',
            avatar: '',
            //存储当前用户是否包含某一个按钮
            buttons:[],
        }
    },
 //获取用户信息方法
        async userInfo() {
            //获取用户信息进行存储仓库当中[用户头像、名字]
            let result: userInfoReponseData = await reqUserInfo();
            //如果获取用户信息成功,存储一下用户信息
            if (result.code == 200) {
                this.username = result.data.name;
                this.avatar = result.data.avatar;
                this.buttons = result.data.buttons;
                //计算当前用户需要展示的异步路由
                let userAsyncRoute = filterAsyncRoute(cloneDeep(asnycRoute), result.data.routes);
                //菜单需要的数据整理完毕
                this.menuRoutes = [...constantRoute, ...userAsyncRoute, anyRoute];
                //目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加
                [...userAsyncRoute, anyRoute].forEach((route: any) => {
                    router.addRoute(route);
                });
                return 'ok';
            } else {
                return Promise.reject(new Error(result.message));
            }
        },
//type.ts
//定义小仓库数据state类型
export interface UserState {
	...,
    buttons:string[]
}
  1. src/permisstion.ts:在路由守卫中获取完用户信息后确保动态添加的异步路由加载完毕再放行
1
2
3
//放行
//万一:刷新的时候是异步路由,有可能获取到用户信息、异步路由还没有加载完毕,出现空白的效果
next({...to});
  1. src/views/404/index.vue:修改404页面
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
    <div class="box">
       <img src="../../assets/images/error_images/404.png" alt="">
       <button @click="goHome">首页</button>
    </div>
</template>

<script setup lang="ts">
import {useRouter} from 'vue-router';
let $router = useRouter();
const goHome = ()=>{
   $router.push('/home')
}
</script>

<style scoped lang="scss">
.box{
  width: 100vw;
  height: 100vh;
  background: white;
  display: flex;
  justify-content: center;
  img{
    width: 800px;
    height: 400px;
  }
  button{
    width: 50px;
    height: 50px;
  }
}
</style>
  1. 新建src/directive/has.ts:添加vue的全局自定义指令实现按钮权限判断:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import pinia from '@/store';
import useUserStore from '@/store/modules/user';
let userStore =useUserStore(pinia)
export const isHasButton = (app: any) => {
    //获取对应的用户仓库
    //全局自定义指令:实现按钮的权限
    app.directive('has', {
        //代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次
        mounted(el:any,options:any) {
            //自定义指令右侧的数值:如果在用户信息buttons数组当中没有
            //从DOM树上干掉
            if(!userStore.buttons.includes(options.value)){
               el.parentNode.removeChild(el);
            }
        },
    })
}
  1. 在入口文件main.ts引入自定义指令文件:
1
2
//引入自定义指令文件
import { isHasButton } from '@/directive/has';
  1. src/views/product/trademark/index.vue:按钮权限的实现
1
            <el-button type="primary" size="default" icon="Plus" @click="addTrademark" v-has="`btn.Trademark.add`">添加品牌</el-button>
Built with Hugo
Theme Stack designed by Jimmy