Vue3做出B站【bilibili】 Vue3+TypeScript+ant-design-vue【快速入门一篇文章精通系列(一)前端项目案例】

本项目分为二部分
1、后台管理系统(用户管理,角色管理,视频管理等)
2、客户端(登录注册、发布视频)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Vue3做出B站【bilibili】 Vue3+TypeScript+ant-design-vue【快速入门一篇文章精通系列(一)前端项目案例】

一、前言

在前端方面我们使用的技术栈包括

TypeScript
Vue3
ant Design Vue
axios
echarts
highcharts
mockjs
pinia
vue-router

二、项目创建基本页面搭建

(一)创建Vue3 + TypeScript项目

1、新建Vue3 项目

npm create vite@latest bilibili-vue3-ts -- --template vue

在这里插入图片描述
将生成的js文件都修改为ts文件
在这里插入图片描述
在这里插入图片描述

2、用WebStorm打开项目

1)打开项目以后执行 npm install

在这里插入图片描述
执行成功
在这里插入图片描述

2)安装TypeScript

安装TypeScript

npm install -g typescript

在这里插入图片描述
安装完成后,在控制台运行如下命令,检查安装是否成功(3.x):

tsc -v

在这里插入图片描述

3)设置一下WebStorm配置

在这里插入图片描述
设置自动编译
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

$FileNameWithoutExtension$.js:$FileNameWithoutExtension$.js.map
$FileDir$

3、配置项目

1)安装依赖

作为前端项目我们使用一些场景的依赖
这里我们只需要将以下依赖复制到package.json,重新运行npm install
将package-lock.json文件夹删除
在这里插入图片描述
在package.json当中
在这里插入图片描述

"dependencies": {
    
    
   "ant-design-vue": "^3.3.0-beta.4",
    "axios": "^0.27.2",
    "echarts": "^5.3.3",
    "echarts-gl": "^2.0.9",
    "highcharts": "^10.2.1",
    "pinia": "^2.0.23",
    "pinia-plugin-persist": "^1.0.0",
    "sass": "^1.54.9",
    "swiper": "^8.4.5",
    "vue": "^3.2.37",
    "vue-router": "^4.1.5",
    "vue3-audio-player": "^1.0.5",
    "vue3-seamless-scroll": "^2.0.1"
  },
  "devDependencies": {
    
    
    "less": "^4.1.3",
    "unplugin-auto-import": "^0.11.2",
    "unplugin-vue-components": "^0.22.4",
    "@vitejs/plugin-vue": "^4.0.0",
    "vite": "^4.0.0"
  }

执行npm install

除了以上安装方式以外,
你也可以自己找到对应依赖的官方网站,
一个一个手动安装

2)路由配置

创建router文件夹
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

import {
    
     createRouter,createWebHashHistory } from 'vue-router';
import Home from "../views/Home.vue";
import About from "../views/About.vue";
//2、定义一些路由
//每个路由都需要映射到一个组件
//我们后面再讨论嵌套路由
const routes = [
    {
    
    path:"/",component:Home,name:"Home"},
    {
    
    path:"/About",component:About,name:"About"},
];
//3、创建路由实例并传递‘routes’配置
//你可以在这里输入更多的配置,但是我们在这里
const router = createRouter({
    
    
    //4、内部提供了 history 模式的实现。为了简单起见,我们在这里使用hash模式
    history:createWebHashHistory(),
    routes, //routes:routes 的缩写
})
export default router

创建Home.vue和About.vue
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

<template>
 <h1>Home</h1>
</template>

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

</script>

<style scoped>

</style>

在这里插入图片描述
修改App.vue
在这里插入图片描述

<script setup lang="ts">
</script>
<template>
  <router-view></router-view>
</template>
<style scoped>
</style>

3)pinia配置

在这里插入图片描述
在这里插入图片描述

import {
    
     createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const store = createPinia()
store.use(piniaPluginPersist)
export default store

在这里插入图片描述
在这里插入图片描述

import {
    
     defineStore } from 'pinia'
export const userStore = defineStore({
    
    
    id: 'user',
    state: () => {
    
    
        return {
    
    
            title: '',
            token:''
        }
    },
    getters: {
    
    
        getTitle: (state) => state.title,
    },
    actions: {
    
    
        setTitle(title:string) {
    
    
            this.title= title
        }
    },
    // 开启数据缓存
    // @ts-ignore
    persist: {
    
     //数据默认存在 sessionStorage 里,并且会以 store 的 id 作为 key
        enabled: true
    }
})

在main.ts当中引入如上内容
在这里插入图片描述

import {
    
     createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index'
import store  from './store/index'
import {
    
     createPinia } from 'pinia'
import * as echarts from 'echarts'

let app = createApp(App)
app.config.globalProperties.$echarts = echarts
app.use(router)
app.use(store)
app.use(createPinia)
app.mount('#app')

4)vite.config.ts配置

在这里插入图片描述

//vite.config.js
import {
    
     defineConfig } from 'vite'
import {
    
    resolve} from 'path'
import vue from '@vitejs/plugin-vue'
import Components from "unplugin-vue-components/vite"
import AutoImport from "unplugin-auto-import/vite"
export default defineConfig({
    
    
  plugins: [
    vue(),
    AutoImport({
    
    
    }),
    Components({
    
    
    }),
  ],
  // ...
  resolve: {
    
    
    alias: {
    
    
      '@': resolve(__dirname, './src')
    }
  },
  server: {
    
    
    port: 80,
    host: true,
    open: true,
    proxy: {
    
    
      '/api': {
    
    
        target: 'http://api.cpengx.cn/metashop/api',
        changeOrigin: true,
        rewrite: (p) => p.replace(/^\/api/, '')
      },
    }
  },
  // 开启less支持
  css: {
    
    
    preprocessorOptions: {
    
    
      less: {
    
    
        javascriptEnabled: true
      }
    }
  }
})

运行测试

npm run dev

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5)按需引入ant-design-vue

在这里插入图片描述

//vite.config.js
import {
    
     defineConfig } from 'vite'
import {
    
    resolve} from 'path'
import vue from '@vitejs/plugin-vue'
import {
    
     AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import Components from "unplugin-vue-components/vite"
import AutoImport from "unplugin-auto-import/vite"
export default defineConfig({
    
    
  plugins: [
    vue(),
    AutoImport({
    
    
      resolvers: [AntDesignVueResolver() ],
    }),
    Components({
    
    
      resolvers: [
        AntDesignVueResolver({
    
    
          importStyle: 'less', // 一定要开启这个配置项
        }),
      ],
    }),
  ],
  // ...
  resolve: {
    
    
    alias: {
    
    
      '@': resolve(__dirname, './src')
    }
  },
  server: {
    
    
    port: 80,
    host: true,
    open: true,
    proxy: {
    
    
      '/api': {
    
    
        target: 'http://api.cpengx.cn/metashop/api',
        changeOrigin: true,
        rewrite: (p) => p.replace(/^\/api/, '')
      },
    }
  },
  // 开启less支持
  css: {
    
    
    preprocessorOptions: {
    
    
      less: {
    
    
        modifyVars: {
    
     // 在这里自定义主题色等样式
          'primary-color': '#fb7299',
          'link-color': '#fb7299',
          'border-radius-base': '2px',
        },
        javascriptEnabled: true,
      }
    }
  }
})

在Home当中放置一个按钮
在这里插入图片描述

<template>
 <h1>Home</h1>
  <a-button type="primary">Primary Button</a-button>
</template>
<script lang="ts" setup name="">
</script>
<style scoped>
</style>

重新运行并访问
在这里插入图片描述

6)安装axios

安装axios:一个基于promise的HTTP库,类ajax

npm install axios

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

import axios from 'axios'
// @ts-ignore
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例

const service = axios.create({
    
    
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    baseURL: "/bilibili-api",
    //baseURL: "/",
    // 超时
    timeout: 10000
})
export default service

配置请求

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

import request from '../utils/request'
/* 有参  */
export const  getXqInfo  = (params:any) => {
    
    
    return request({
    
    
        method: "GET",
        url: "/grid/openApi/screen/getXqInfo",
        params,
    });
};
/* 无参  */
export const getCommunityOverview = ( ) => {
    
    
    return request({
    
    
        method: "GET",
        url: "/grid/openApi/screen/getCommunityOverview",
    });
};

4、设置页面路由

删除页面的自动创建好的页面
在这里插入图片描述
在这里插入图片描述
设置路由
在这里插入图片描述

import {
    
     createRouter,createWebHashHistory } from 'vue-router';
import Home from "../views/Home.vue";
import Login from "../views/Login.vue";
//2、定义一些路由
//每个路由都需要映射到一个组件
//我们后面再讨论嵌套路由
const routes = [
    {
    
    path:"/",component:Home,name:"Home"},
    {
    
    path:"/login",component:Login,name:"Login"},
];
//3、创建路由实例并传递‘routes’配置
//你可以在这里输入更多的配置,但是我们在这里
const router = createRouter({
    
    
    //4、内部提供了 history 模式的实现。为了简单起见,我们在这里使用hash模式
    history:createWebHashHistory(),
    routes, //routes:routes 的缩写
})
export default router

(二)实现登录页面

1、设置登陆页面

我们找到From表单的内容
https://www.antdv.com/components/form-cn
在这里插入图片描述

在这里插入图片描述

复制上述代码,但是我们并不会直接使用期内容

在这里插入图片描述

<template>
  <a-form
      ref="formRef"
      name="custom-validation"
      :model="formState"
      :rules="rules"
      v-bind="layout"
      @finish="handleFinish"
      @validate="handleValidate"
      @finishFailed="handleFinishFailed"
  >
    <a-form-item has-feedback label="Password" name="pass">
      <a-input v-model:value="formState.pass" type="password" autocomplete="off" />
    </a-form-item>
    <a-form-item has-feedback label="Confirm" name="checkPass">
      <a-input v-model:value="formState.checkPass" type="password" autocomplete="off" />
    </a-form-item>
    <a-form-item has-feedback label="Age" name="age">
      <a-input-number v-model:value="formState.age" />
    </a-form-item>
    <a-form-item :wrapper-col="{ span: 14, offset: 4 }">
      <a-button type="primary" html-type="submit">Submit</a-button>
      <a-button style="margin-left: 10px" @click="resetForm">Reset</a-button>
    </a-form-item>
  </a-form>
</template>

<script lang="ts" setup>
import type {
    
     Rule } from 'ant-design-vue/es/form';
import {
    
      reactive, ref } from 'vue';
import type {
    
     FormInstance } from 'ant-design-vue';

interface FormState {
    
    
  pass: string;
  checkPass: string;
  age: number | undefined;
}

const formRef = ref<FormInstance>();

const formState = reactive<FormState>({
    
    
  pass: '',
  checkPass: '',
  age: undefined,
});

let checkAge = async (_rule: Rule, value: number) => {
    
    
  if (!value) {
    
    
    return Promise.reject('Please input the age');
  }
  if (!Number.isInteger(value)) {
    
    
    return Promise.reject('Please input digits');
  } else {
    
    
    if (value < 18) {
    
    
      return Promise.reject('Age must be greater than 18');
    } else {
    
    
      return Promise.resolve();
    }
  }
};

let validatePass = async (_rule: Rule, value: string) => {
    
    
  if (value === '') {
    
    
    return Promise.reject('Please input the password');
  } else {
    
    
    if (formState.checkPass !== '') {
    
    
      formRef.value!.validateFields('checkPass');
    }
    return Promise.resolve();
  }
};

let validatePass2 = async (_rule: Rule, value: string) => {
    
    
  if (value === '') {
    
    
    return Promise.reject('Please input the password again');
  } else if (value !== formState.pass) {
    
    
    return Promise.reject("Two inputs don't match!");
  } else {
    
    
    return Promise.resolve();
  }
};

const rules: Record<string, Rule[]> = {
    
    
  pass: [{
    
     required: true, validator: validatePass, trigger: 'change' }],
  checkPass: [{
    
     validator: validatePass2, trigger: 'change' }],
  age: [{
    
     validator: checkAge, trigger: 'change' }],
};

const layout = {
    
    
  labelCol: {
    
     span: 4 },
  wrapperCol: {
    
     span: 14 },
};

const handleFinish = (values: FormState) => {
    
    
  console.log(values, formState);
};

const handleFinishFailed = (errors: any) => {
    
    
  console.log(errors);
};

const resetForm = () => {
    
    
  formRef.value!.resetFields();
};

const handleValidate = (...args: any[]) => {
    
    
  console.log(args);
};
</script>
<style scoped>

</style>

访问页面http://localhost/#/login
在这里插入图片描述
删除style.css当中样式
在这里插入图片描述
在这里插入图片描述
调整一下页面
在这里插入图片描述

<template>

  <a-card style="width: 800px;margin:10% auto;border-radius: 15px;">
    <div style="width: 200px;margin: auto">
      <a-image
          style="margin: auto"
          :width="200"
          :preview="false"
          src="src/assets/bilibili.png"
      />
    </div>
    <div class="from-item">

      <a-form
          :model="formState"
          name="normal_login"
          class="login-form"
          @finish="onFinish"
          @finishFailed="onFinishFailed"
      >
        <a-form-item
            label="账号"
            name="username"
            :rules="[{ required: true, message: '请输入账号!' }]"
        >
          <a-input v-model:value="formState.username">
            <template #prefix>
              <UserOutlined class="site-form-item-icon" />
            </template>
          </a-input>
        </a-form-item>
        <a-form-item
            label="密码"
            name="password"
            :rules="[{ required: true, message: '请输入密码!' }]"
        >
          <a-input-password v-model:value="formState.password">
            <template #prefix>
              <LockOutlined class="site-form-item-icon" />
            </template>
          </a-input-password>
        </a-form-item>
        <a-row>
          <a-col :span="12">
            <a-form-item
                label="验证码"
                name="code"
                :rules="[{ required: true, message: '请输入验证码!' }]"
            >
              <a-input  v-model:value="formState.code" placeholder="请输入验证码" >

              </a-input>
            </a-form-item>
          </a-col>
          <a-col :span="12">
            <a-image
                :width="60"
                style="height: 30px;margin-left: 10%"
                :preview="false"
                src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
            />
          </a-col>
        </a-row>
        <a-form-item>
          <a-row>
            <a-col :span="6"></a-col>
            <a-col :span="12">
              <a-button  type="primary" block html-type="submit" class="login-form-button">
                登录
              </a-button>
            </a-col>
            <a-col :span="6"></a-col>
          </a-row>

        </a-form-item>
      </a-form>
    </div>

  </a-card>
</template>

<script lang="ts" setup>
import {
    
     defineComponent, reactive, computed } from 'vue';
import {
    
     UserOutlined, LockOutlined } from '@ant-design/icons-vue';
interface FormState {
    
    
  username: string;
  password: string;
  code: string;
}
const formState = reactive<FormState>({
    
    
  username: '',
  password: '',
  code: '',
});
const onFinish = (values: any) => {
    
    
  console.log('Success:', values);
};

const onFinishFailed = (errorInfo: any) => {
    
    
  console.log('Failed:', errorInfo);
};
const disabled = computed(() => {
    
    
  return !(formState.username && formState.password);
});
</script>

<style scoped>
.from-item{
    
    
  padding-top: 10%;
  margin: auto;
  width: 60%;
}

#components-form-demo-normal-login .login-form {
    
    
  max-width: 300px;
}
#components-form-demo-normal-login .login-form-forgot {
    
    
  float: right;
}
#components-form-demo-normal-login .login-form-button {
    
    
  width: 100%;
}
</style>

在这里插入图片描述

2、设置登录请求

在这里插入图片描述

import request from '@/utils/request'
/* 无参  */
export const getCaptchaImg = ( ) => {
    
    
    return request({
    
    
        method: "GET",
        url: "/captcha",
    });
};

3、创建mock.ts

安装qs
qs:查询参数序列化和解析库

npm install qs

安装mockjs
mockjs:为我们生成随机数据的工具库

npm install mockjs

在这里插入图片描述
在main.ts当中引入mock.ts
在这里插入图片描述

import "@/mock"

完善mock.ts
在这里插入图片描述

// @ts-ignore
import Mock from "mockjs";

const Random = Mock.Random

let Result = {
    
    
    code: 200,
    msg: '操作成功',
    data: null
}

Mock.mock('/bilibili-api/captcha','get',()=>{
    
    
    // @ts-ignore
    Result.data = {
    
    
        token: Random.string(32),
        captchaImg:Random.dataImage('120x40','p7n5w')
    }
    return Result;
})

4、显示验证码

完善request.ts,设置发起请求的内容
在这里插入图片描述

import axios from 'axios'
// @ts-ignore
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例

const service = axios.create({
    
    
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    baseURL: "/bilibili-api",
    //baseURL: "/",
    // 超时
    timeout: 10000
})
export default service

完善vite.config.ts,设置发起请求的路径和地址
在这里插入图片描述

proxy: {
    
    
      '/bilibili-api': {
    
    
        target: 'http://localhost:8081',
        changeOrigin: true,
        rewrite: (p) => p.replace(/^\/bilibili-api/, '')
      },
}

完善src/api/index.ts设置请求
在这里插入图片描述

import request from '@/utils/request'
/*无参*/
export const getCaptchaImg = () => {
    
    
    return request({
    
    
        method: "GET",
        url: "/captcha",
    });
};

设置登录页面完善请求内容
在这里插入图片描述

 <a-image
                :width="60"
                style="height: 30px;margin-left: 10%"
                :preview="false"
                :src="captchaImg"
            />

在这里插入图片描述
在这里插入图片描述

import {
    
    getCaptchaImg} from "@/api";
const getCaptcha = () => {
    
    
  getCaptchaImg().then(res => {
    
    
    formState.token = res.data.data.token;
    captchaImg.value = res.data.data.captchaImg;
  })
}
onMounted(()=>{
    
    
  getCaptcha();
})

访问http://localhost/#/login
在这里插入图片描述

5、在store当中的store.ts设置SET_TOKEN

在这里插入图片描述

SET_TOKEN(token:string ){
    
    
            this.token = token
            localStorage.setItem("token",token)
 },

6、实现登录请求相关内容

在这里插入图片描述

export const userLogin = (data:any) => {
    
    
    return request({
    
    
        url: '/login',
        method: 'post',
        data: data
    })
};

在这里插入图片描述

const router = useRouter();
import {
    
     useRouter } from "vue-router";
const user = userStore()

const router = useRouter();
const onFinish = (values: any) => {
    
    
  userLogin(formState).then(res => {
    
    
    const jwt = res.headers['authorization']
    user.SET_TOKEN(jwt);
    router.push("/");
  })
};

完善mock.ts
在这里插入图片描述

Mock.mock('/bilibili-api/login','post',()=>{
    
    
    Result.code = 404
    Result.msg = "验证码错误"
    return Result;
})

7、完善登录

(1)在main.ts当中引入antd的全局样式

在这里插入图片描述

import 'ant-design-vue/dist/antd.css';

(2)完善request.ts当中响应的内容

在这里插入图片描述

import axios from 'axios'
import {
    
      message as Message, notification } from 'ant-design-vue';
import {
    
     useRouter } from "vue-router";
// @ts-ignore
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例

const service = axios.create({
    
    
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    baseURL: "/bilibili-api",
    // 超时
    timeout: 10000
})
service.interceptors.request.use(config => {
    
    
    // @ts-ignore
    config.headers['Authorization'] = localStorage.getItem("token")
    return config;
});
service.interceptors.response.use(response => {
    
    
        let res = response.data
        if (res.code === 200) {
    
    
            return response
        } else {
    
    
            Message.error(!res.msg ? '系统异常' : res.msg)
            return Promise.reject(response.data.msg)
        }
    }, error => {
    
    
        if (error.response.data) {
    
    
            error.message = error.response.data.msg
        }
        if (error.response.status === 401) {
    
    
            useRouter().push("/login")
        }
        Message.error(error.message)
        return Promise.reject(error)
    }
)
export default service

运行测试
http://localhost/#/login
在这里插入图片描述

三、后台管理界面开发

(一)创建index页面

一般来说,管理系统的页面我们都是头部是一个简单的信息展示系统名称和登录用户信息,然后中间的左边是菜单导航栏,右边是内容,对应到ant Design Vue的组件中,我们可以找到这个Layout 布局容器用于布局,方便快速搭建页面的基本结构。

而我们采用这个布局:
在这里插入图片描述

1、新建index页面

在这里插入图片描述
在这里插入图片描述

<template>
  <div>
    <a-layout>
      <a-layout-sider>Sider</a-layout-sider>
      <a-layout>
        <a-layout-header>Header</a-layout-header>
        <a-layout-content>Content</a-layout-content>
        <a-layout-footer>Footer</a-layout-footer>
      </a-layout>
    </a-layout>
  </div>
</template>
<script name="index" setup lang="ts">
</script>
<style scoped>
#components-layout-demo-basic .code-box-demo {
    
    
  text-align: center;
}
#components-layout-demo-basic .ant-layout-header,
#components-layout-demo-basic .ant-layout-footer {
    
    
  color: #fff;
  background: #fa81a3;
}
[data-theme='dark'] #components-layout-demo-basic .ant-layout-header {
    
    
  background: #fb7299;
}
[data-theme='dark'] #components-layout-demo-basic .ant-layout-footer {
    
    
  background: #fb7299;
}
#components-layout-demo-basic .ant-layout-footer {
    
    
  line-height: 1.5;
}
#components-layout-demo-basic .ant-layout-sider {
    
    
  color: #fff;
  line-height: 120px;
  background: #fd4c7e;
}
[data-theme='dark'] #components-layout-demo-basic .ant-layout-sider {
    
    
  background: #d9456f;
}
#components-layout-demo-basic .ant-layout-content {
    
    
  min-height: 120px;
  color: #fff;
  line-height: 120px;
  background: #b64665;
}
[data-theme='dark'] #components-layout-demo-basic .ant-layout-content {
    
    
  background: #cc889b;
}
#components-layout-demo-basic > .code-box-demo > .ant-layout + .ant-layout {
    
    
  margin-top: 48px;
}
</style>

2、设置路由

在这里插入图片描述

router.push("/index");

在这里插入图片描述
设置一下状态码使其跳转成功
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3、完善菜单页面内容

在这里插入图片描述

<template>
  <a-layout has-sider>
    <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
      <div class="logo" />
      <a-menu v-model:selectedKeys="selectedKeys" theme="dark" style="height: 100vh" mode="inline">
        <a-menu-item key="app" disabled>
          <div >
            bilibili后台管理系统
          </div>
        </a-menu-item>
        <a-menu-item key="1">
          <template #icon>
            <MailOutlined />
          </template>
         主页
        </a-menu-item>
        <a-sub-menu key="sub1">
          <template #icon>
            <AppstoreOutlined />
          </template>
          <template #title>系统管理</template>
          <a-menu-item key="3">用户管理</a-menu-item>
          <a-menu-item key="4">角色管理</a-menu-item>
          <a-menu-item key="5">菜单管理</a-menu-item>
        </a-sub-menu>
        <a-sub-menu key="sub2">
          <template #icon>
            <SettingOutlined />
          </template>
          <template #title>系统工具</template>
          <a-menu-item key="7">数字字典</a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider><a-layout-content :style="{  marginTop: '0' }">
      <a-menu
          v-model:selectedKeys="selectedKeysTop"
          theme="dark"
          mode="horizontal"
          :style="{ lineHeight: '64px',marginLeft:'-15px' }"
      >
        <a-sub-menu key="sub2" >
          <template #title>
            <div >
              <menu-unfold-outlined
                  v-if="collapsed"
                  class="trigger"
                  @click="() => (collapsed = !collapsed)"
              />
              <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
            </div>
          </template>
        </a-sub-menu>
        <a-sub-menu key="sub1" style="margin-left: 85%" >
          <template #title>
            <a-avatar style="background-color: #87d068">
              <template #icon>
                <UserOutlined />
              </template>
            </a-avatar>
            <a class="ant-dropdown-link" @click.prevent>
              admin
              <DownOutlined />
            </a>
          </template>
          <a-menu-item key="setting:1">Option 1</a-menu-item>
          <a-menu-item key="setting:2">Option 2</a-menu-item>
          <a-menu-item key="setting:3">Option 3</a-menu-item>
          <a-menu-item key="setting:4">Option 4</a-menu-item>
        </a-sub-menu>
      </a-menu>
        <a-breadcrumb :style="{ margin: '16px 0' }">
          <a-breadcrumb-item>Home</a-breadcrumb-item>
          <a-breadcrumb-item>List</a-breadcrumb-item>
          <a-breadcrumb-item>App</a-breadcrumb-item>
        </a-breadcrumb>
        <div :style="{ background: '#fff', padding: '24px', minHeight: '800px' }">Content</div>
    </a-layout-content>
  </a-layout>
</template>
<script lang="ts" setup>
import {
    
    
  DownOutlined,
  UserOutlined,
  VideoCameraOutlined,
  UploadOutlined,
  MenuUnfoldOutlined,
  MenuFoldOutlined,
} from '@ant-design/icons-vue';
import {
    
     defineComponent, ref } from 'vue';
let selectedKeys =ref<string[]>(['1'])
let selectedKeysTop =ref<string[]>(['1'])
let collapsed =ref<boolean>(false)
</script>
<style>
#components-layout-demo-custom-trigger .trigger {
    
    
  font-size: 18px;
  line-height: 64px;
  padding: 0 24px;
  cursor: pointer;
  transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
    
    
  color: #fb7299;
}
#components-layout-demo-custom-trigger .logo {
    
    
  height: 32px;
  background: rgba(255, 255, 255, 0.3);
  margin: 16px;
}
.site-layout .site-layout-background {
    
    
  background: #fff;
}
</style>

在这里插入图片描述

4、Vue代码抽取

(1)抽取菜单

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

<template>
  <a-menu v-model:selectedKeys="selectedKeys" theme="dark" style="height: 100vh" mode="inline">
    <a-menu-item key="app" disabled>
      <template #icon>
        <img src="../../assets/bilibilitoum.png" style="height: 15px;width: 35px">
      </template>
      <div >
        后台管理系统
      </div>
    </a-menu-item>
    <a-menu-item key="1">
      <template #icon>
        <MailOutlined />
      </template>
      主页
    </a-menu-item>
    <a-sub-menu key="sub1">
      <template #icon>
        <AppstoreOutlined />
      </template>
      <template #title>系统管理</template>
      <a-menu-item key="3">用户管理</a-menu-item>
      <a-menu-item key="4">角色管理</a-menu-item>
      <a-menu-item key="5">菜单管理</a-menu-item>
    </a-sub-menu>
    <a-sub-menu key="sub2">
      <template #icon>
        <SettingOutlined />
      </template>
      <template #title>系统工具</template>
      <a-menu-item key="7">数字字典</a-menu-item>
    </a-sub-menu>
  </a-menu>
</template>
<script setup lang="ts">
import {
    
    ref} from "vue";
import {
    
    
  AppstoreOutlined,
  MailOutlined,
  SettingOutlined,
} from '@ant-design/icons-vue';
let selectedKeys =ref<string[]>(['1'])
let selectedKeysTop =ref<string[]>(['1'])
let collapsed =ref<boolean>(false)
</script>
<style scoped>
</style>

在这里插入图片描述
在这里插入图片描述

<SideMenu></SideMenu>
import SideMenu from "./inc/SideMenu.vue"

在这里插入图片描述

(2)index

将index.vue的内容全部抽取到Home
在这里插入图片描述

<template>
  <a-layout has-sider>
    <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
      <div class="logo" />
      <SideMenu></SideMenu>
    </a-layout-sider><a-layout-content :style="{  marginTop: '0' }">
      <a-menu
          v-model:selectedKeys="selectedKeysTop"
          theme="dark"
          mode="horizontal"
          :style="{ lineHeight: '64px',marginLeft:'-15px' }"
      >
        <a-sub-menu key="sub2" >
          <template #title>
            <div >
              <menu-unfold-outlined
                  v-if="collapsed"
                  class="trigger"
                  @click="() => (collapsed = !collapsed)"
              />
              <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
            </div>
          </template>
        </a-sub-menu>
        <a-sub-menu key="sub1" style="margin-left: 85%" >
          <template #title>
            <a-avatar style="background-color: #87d068">
              <template #icon>
                <UserOutlined />
              </template>
            </a-avatar>
            <a class="ant-dropdown-link" @click.prevent>
              admin
              <DownOutlined />
            </a>
          </template>
          <a-menu-item key="setting:1">Option 1</a-menu-item>
          <a-menu-item key="setting:2">Option 2</a-menu-item>
          <a-menu-item key="setting:3">Option 3</a-menu-item>
          <a-menu-item key="setting:4">Option 4</a-menu-item>
        </a-sub-menu>
      </a-menu>
      <a-breadcrumb :style="{ margin: '16px 0' }">
        <a-breadcrumb-item>Home</a-breadcrumb-item>
        <a-breadcrumb-item>List</a-breadcrumb-item>
        <a-breadcrumb-item>App</a-breadcrumb-item>
      </a-breadcrumb>
      <div :style="{ background: '#fff', padding: '24px', minHeight: '800px' }">Content</div>
    </a-layout-content>
  </a-layout>
</template>
<script lang="ts" setup>
import {
    
    
  DownOutlined,
  UserOutlined,
  VideoCameraOutlined,
  UploadOutlined,
  MenuUnfoldOutlined,
  MenuFoldOutlined,
} from '@ant-design/icons-vue';
import SideMenu from "./inc/SideMenu.vue"
import {
    
     defineComponent, ref } from 'vue';
let selectedKeys =ref<string[]>(['1'])
let selectedKeysTop =ref<string[]>(['1'])
let collapsed =ref<boolean>(false)
</script>
<style>
#components-layout-demo-custom-trigger .trigger {
    
    
  font-size: 18px;
  line-height: 64px;
  padding: 0 24px;
  cursor: pointer;
  transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
    
    
  color: #fb7299;
}
#components-layout-demo-custom-trigger .logo {
    
    
  height: 32px;
  background: rgba(255, 255, 255, 0.3);
  margin: 16px;
}
.site-layout .site-layout-background {
    
    
  background: #fff;
}
</style>

5、设置子路由

在这里插入图片描述

		children:[
            {
    
    
                path:'/index',
                name:'Index',
                component:Index
            }
        ]

在Home.vue当中设置路由
在这里插入图片描述

 <router-view>
</router-view>

访问页面:http://localhost/#/index
在这里插入图片描述

6、编写导航栏路由

1)创建需要路由跳转的页面

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

防止手残贴上全部代码

import  {
    
     createRouter,createWebHashHistory } from 'vue-router';
import Home from "../views/Home.vue";
import Index from "../views/Index.vue";
import Login from "../views/Login.vue";
import Menu from '../views/sys/Menu.vue'
import Role from '../views/sys/Role.vue'
import User from '../views/sys/User.vue'
//2、定义一些路由
//每个路由都需要映射到一个组件
//我们后面再讨论嵌套路由
const routes = [
    {
    
    
        path:"/",
        component:Home,
        name:"Home",
        children:[
            {
    
    
                path:'',
                name:'Index',
                component:Index
            },
            {
    
    
                path:'/index',
                name:'Index',
                component:Index
            },
            {
    
    
                path:'/users',
                name:'SysUser',
                component:User
            },
            {
    
    
                path:'/roles',
                name:'SysRole',
                component:Role
            },
            {
    
    
                path:'/menus',
                name:'SysMenu',
                component:Menu
            }
        ]
    },
    {
    
    path:"/login",component:Login,name:"Login"},
];
//3、创建路由实例并传递‘routes’配置
//你可以在这里输入更多的配置,但是我们在这里
const router = createRouter({
    
    
    //4、内部提供了 history 模式的实现。为了简单起见,我们在这里使用hash模式
    history:createWebHashHistory(),
    routes, //routes:routes 的缩写
})
export default router

访问:http://localhost/#/roles
在这里插入图片描述

访问: http://localhost/#/users
在这里插入图片描述
访问:http://localhost/#/menus

2)设置页面路由

在这里插入图片描述

	<router-link to="/index">
      <a-menu-item key="1">
        <template #icon>
          <MailOutlined />
        </template>
        主页
      </a-menu-item>
    </router-link>
    <a-sub-menu key="sub1">
      <template #icon>
        <AppstoreOutlined />
      </template>
      <template #title>系统管理</template>
      <router-link to="/users">
        <a-menu-item key="3">
          <template #icon>
            <UserOutlined />
          </template>
          用户管理
        </a-menu-item>
      </router-link>
      <router-link to="/roles">
        <a-menu-item key="4">
          <template #icon>
            <TeamOutlined />
          </template>
          角色管理
        </a-menu-item>
      </router-link>
      <router-link to="/menus">
        <a-menu-item key="5">
          <template #icon>
            <MenuOutlined />
          </template>
          菜单管理
        </a-menu-item>
      </router-link>
    </a-sub-menu>
    <a-sub-menu key="sub2">
      <template #icon>
        <SettingOutlined />
      </template>
      <template #title>系统工具</template>
      <a-menu-item key="7">
        <template #icon>
          <ContainerOutlined />
        </template>
        数字字典
      </a-menu-item>
    </a-sub-menu>

点击测试
在这里插入图片描述
在这里插入图片描述

(二)用户登录信息展示

管理界面的右上角的用户信息现在是写死的,
因为我们现在已经登录成功,所以我们可以通过接口去请求获取到当前的用户信息了,
这样我们就可以动态显示用户的信息,这个接口比较简单,然后退出登录的链接也一起完成,
就请求接口同时把浏览器中的缓存删除就退出了哈。

1、完善用户接口

在这里插入图片描述

export const getUserInfo = () => {
    
    
    return request({
    
    
        url: '/sys/userInfo',
        method: 'get',
    })
};

2、设置mock.js

在这里插入图片描述


Mock.mock('/bilibili-api/sys/userInfo','get',()=>{
    
    
    // @ts-ignore
    Result.data = {
    
    
        id:"1",
        username:"itbluebox",
        avatar:"https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
    }
    return Result
})

3、设置个人中心的路由

在这里插入图片描述

<a-menu-item key="setting:1">
       <router-link to="/userCenter">
             个人中心
        </router-link>
</a-menu-item>

在这里插入图片描述

import UserCenter from '../views/UserCenter.vue'
{
    
    
    path:'/userCenter',
    name:'UserCenter',
    component:UserCenter
},

创建对应的页面
在这里插入图片描述
在这里插入图片描述

<template>
  <a-form
      ref="formRef"
      :model="formState"
      :label-col="labelCol"
      :wrapper-col="wrapperCol"
      :rules="rules"
  >
    <a-form-item ref="user" label="账号" name="user">
      <a-input v-model:value="formState.user" placeholder="Username">
        <template #prefix><UserOutlined style="color: rgba(0, 0, 0, 0.25)" /></template>
      </a-input>
    </a-form-item>
    <a-form-item ref="password" label="密码" name="password">
      <a-input v-model:value="formState.password" type="password" placeholder="Password">
        <template #prefix><LockOutlined style="color: rgba(0, 0, 0, 0.25)" /></template>
      </a-input>
    </a-form-item>
    <a-form-item ref="code" label="验证码" name="code">
      <a-row>
        <a-col :span="12">
          <a-input v-model:value="formState.code" type="textarea" />
        </a-col>
        <a-col :span="12">
          <a-image @click="getCaptcha"
                   :width="60"
                   style="height: 30px;margin-left: 10%"
                   :preview="false"
                   :src="captchaImg"
          />
        </a-col>
      </a-row>
    </a-form-item>
    <a-form-item :wrapper-col="{ span: 14, offset: 4 }">
      <a-button type="primary" @click="onSubmit">修改</a-button>
      <a-button style="margin-left: 10px" @click="onReSet">重置</a-button>
    </a-form-item>
  </a-form>
</template>

<script name="UserCenter" lang="ts" setup>
import {
    
     defineComponent, reactive, toRaw, UnwrapRef,ref,onMounted } from 'vue';
import {
    
     ValidateErrorEntity } from 'ant-design-vue/es/form/interface';
import {
    
    getCaptchaImg} from "@/api";
onMounted(()=>{
    
    
  getCaptcha()
})
const formRef = ref();
let captchaImg = ref('')
let labelCol  = reactive(
    {
    
     span: 4 },
)
let wrapperCol  = reactive(
    {
    
     span: 14  },
)
interface FormState {
    
    
  user: string;
  password: string | undefined;
  code:  undefined;
}
const formState: UnwrapRef<FormState> = reactive({
    
    
  user: '',
  password: undefined,
  code: undefined,
});
const rules = reactive({
    
    
  user: [
    {
    
    required: true, message: '请输入用户名', trigger: 'blur'},
  ],
  password: [{
    
    required: true, message: '请输入密码', trigger: 'blur'}],
  code: [{
    
    required: true, message: '请输入密码', trigger: 'blur'}],
})
const onSubmit = () => {
    
    
  formRef.value.validate()
      .then(() => {
    
    
        console.log('values', formState, toRaw(formState));
      })
      .catch((error: ValidateErrorEntity<FormState>) => {
    
    
        console.log('error', error);
      });
};
const onReSet = () => {
    
    
  console.log('submit!', toRaw(formState));
};
const getCaptcha = () => {
    
    
  getCaptchaImg().then(res => {
    
    
    captchaImg.value = res.data.data.captchaImg;
  })
}
</script>
<style scoped>
</style>

在这里插入图片描述
在这里插入图片描述

4、设置退出登录

在这里插入图片描述

export const logout = () => {
    
    
    return request({
    
    
        url: '/logout',
        method: 'get',
    })
};

在这里插入图片描述

<a-menu-item key="setting:2" @click.native="logOut">退出</a-menu-item>

在这里插入图片描述

import {
    
     userStore} from '@/store/store'
const user = userStore()
const logOut = () => {
    
    
  logout().then(response => {
    
    
    user.resetState()
    localStorage.clear();
    sessionStorage.clear();
    router.push("/login");
  });
}

设置Store的状态
在这里插入图片描述

 resetState(){
    
    
            this.token = ""
 },

设置mock
在这里插入图片描述

Mock.mock('/bilibili-api/logout','get',()=>{
    
    
    return Result;
})

(三)动态菜单开发

1、修改一下路由规则

在这里插入图片描述

{
    
    
                path:'/sys/users',
                name:'SysUser',
                component:User
            },
            {
    
    
                path:'/sys/roles',
                name:'SysRole',
                component:Role
            },
            {
    
    
                path:'/sys/menus',
                name:'SysMenu',
                component:Menu
            }

2、设置动态菜单的数据

(1)自定义Icon组件

在这里插入图片描述
目前先这样。后期会对其进行优化
在这里插入图片描述

<template>
  <div>
    <SettingOutlined v-if="iconName == 'setting-outlined'"></SettingOutlined>
    <UserOutlined v-if="iconName == 'user-outlined'"></UserOutlined>
    <MenuOutlined v-if="iconName == 'menu-outlined'"></MenuOutlined>
    <ContainerOutlined v-if="iconName == 'container-outlined'"></ContainerOutlined>
    <UsergroupAddOutlined v-if="iconName == 'user-group-add-outlined'"></UsergroupAddOutlined>
  </div>
</template>

<script setup lang="ts">
import {
    
    ref,reactive} from "vue";
import {
    
    
  UserOutlined,
  SettingOutlined,
  MenuOutlined,
  ContainerOutlined,
 UsergroupAddOutlined
} from '@ant-design/icons-vue';
const props = defineProps<{
      
      
  iconName: any;
}>();
</script>

<style scoped>

</style>

(2)SideMenu.vue菜单页面

在这里插入图片描述

在这里插入图片描述

<template>
  <a-menu v-model:selectedKeys="selectedKeys" theme="dark" style="height: 100vh" mode="inline">
    <a-menu-item key="app" disabled>
      <template #icon>
        <img src="../../assets/bilibilitoum.png" style="height: 15px;width: 35px">
      </template>
      <div >
        后台管理系统
      </div>
    </a-menu-item>
    <router-link to="/index">
      <a-menu-item key="1">
        <template #icon>
          <MailOutlined />
        </template>
        主页
      </a-menu-item>
    </router-link>
    <a-sub-menu :key="menu.name"  v-for="menu in menuList.menus">
      <template #icon>
        <Icon :icon-name="menu.icon" />
      </template>
      <template #title>{
    
    {
    
    menu.title}}</template>
      <router-link :to="item.path" v-for="item in menu.children">
        <a-menu-item :key="item.key">
          <template #icon>
            <Icon :icon-name="item.icon" />
          </template>
          {
    
    {
    
    item.title}}
        </a-menu-item>
      </router-link>
    </a-sub-menu>
  </a-menu>
</template>
<script setup lang="ts">
import {
    
    ref,reactive} from "vue";
import {
    
    
  AppstoreOutlined,
  MailOutlined,
  SettingOutlined,
  TeamOutlined,
  UserOutlined,
  MenuOutlined,
  ContainerOutlined
} from '@ant-design/icons-vue';
import Icon from "@/components/Icon.vue"
let selectedKeys =ref<string[]>(['1'])
let selectedKeysTop =ref<string[]>(['1'])
let collapsed =ref<boolean>(false)
let menuList = reactive({
    
    
  menus:  [{
    
    
        key:101,
        title: '系统管理',
        name: 'SysMange',
        icon: 'setting-outlined',
        path: '',
        children: [
          {
    
    
            key:102,
            title: '用户管理',
            name: 'SysUser',
            icon: 'user-outlined',
            path: '/sys/users',
            children: []
          },
          {
    
    
            key:103,
            title: '角色管理',
            name: 'SysUser',
            icon: 'user-group-add-outlined',
            path: '/sys/roles',
            children: []
          },
          {
    
    
            key:104,
            title: '菜单管理',
            name: 'SysMenu',
            icon: 'menu-outlined',
            path: '/sys/menus',
            children: []
          }
        ]
      },
        {
    
    
          key:201,
          title: '系统工具',
          name: 'SysTools',
          icon: 'menu-outlined',
          path: '',
          children: [{
    
    
            title: '数字字典',
            name: 'SysDict',
            icon: 'container-outlined',
            path: '/sys/dicts',
            children: []
          }]
        }
      ]
})
</script>
<style scoped>
</style>

刷新并访问页面
在这里插入图片描述

(3)创建保存菜单的状态信息的内容

在这里插入图片描述

menuList:[],
            authoritys:[]

 setMenuList(menuList:any) {
    
    
            this.menuList = menuList
        },
        setAuthoritys(authoritys:any) {
    
    
            this.authoritys = authoritys
        },

发送获取菜单的请求
在这里插入图片描述

export const nav = () => {
    
    
    return request({
    
    
        url: '/sys/menu/nav',
        method: 'get',
    })
};

在路由当中获取拿到menuList,
在这里插入图片描述

import {
    
    nav} from "@/api";
import {
    
     userStore} from '@/store/store'

设置在路由加载前拿到前,拿到菜单的内容并添加到Store
在这里插入图片描述

router.beforeEach((to,from,next)=>{
    
    
    nav().then(res => {
    
    
        //拿到menuList
        userStore().setMenuList(res.data.data.nav)
        userStore().setAuthoritys(res.data.data.authoritys)
    })
    next()
})

(4)设置mockjs

在这里插入图片描述


Mock.mock('/bilibili-api/sys/menu/nav', 'get', () => {
    
    
    let nav = [
        {
    
    
            key:101,
            title: '系统管理',
            name: 'SysMange',
            icon: 'setting-outlined',
            path: '',
            children: [
                {
    
    
                    key:102,
                    title: '用户管理',
                    name: 'SysUser',
                    icon: 'user-outlined',
                    path: '/sys/users',
                    children: []
                },
                {
    
    
                    key:103,
                    title: '角色管理',
                    name: 'SysUser',
                    icon: 'user-group-add-outlined',
                    path: '/sys/roles',
                    children: []
                },
                {
    
    
                    key:104,
                    title: '菜单管理',
                    name: 'SysMenu',
                    icon: 'menu-outlined',
                    path: '/sys/menus',
                    children: []
                }
            ]
        },
        {
    
    
            key:201,
            title: '系统工具',
            name: 'SysTools',
            icon: 'menu-outlined',
            path: '',
            children: [{
    
    
                title: '数字字典',
                name: 'SysDict',
                icon: 'container-outlined',
                path: '/sys/dicts',
                children: []
            }]
        }

    ];
    // @ts-ignore
    let authoritys = [];
    // @ts-ignore
    Result.data = {
    
    
        nav: nav,
        // @ts-ignore
        authoritys: authoritys
    }
    return Result;
})

(5)完善SideMenu.vue菜单页面,设置请求并渲染菜单

在这里插入图片描述

import {
    
    ref,reactive,onMounted} from "vue";
import {
    
    
  AppstoreOutlined,
  MailOutlined,
  SettingOutlined,
  TeamOutlined,
  UserOutlined,
  MenuOutlined,
  ContainerOutlined
} from '@ant-design/icons-vue';
import Icon from "@/components/Icon.vue"
import {
    
     userStore} from '@/store/store'
let selectedKeys =ref<string[]>(['1'])
let selectedKeysTop =ref<string[]>(['1'])
let collapsed =ref<boolean>(false)
let menuList = reactive({
    
    
     menus:  []
})
onMounted(()=>{
    
    
  menuList.menus = userStore().getMenuList
})

在这里插入图片描述

3、设置动态路由加载一次以后无需二次加载

在这里插入图片描述

hasRoutes:false

 getHasRoutes: (state) => state.hasRoutes,

 changeRouteStatus(hasRoutes:any){
    
    
            this.hasRoutes = hasRoutes;
}

在这里插入图片描述

router.beforeEach((to,from,next)=>{
    
    
    let hasRoutes = userStore().getHasRoutes;
    if(!hasRoutes){
    
    
        nav().then(res => {
    
    
            //拿到menuList
            userStore().setMenuList(res.data.data.nav)
            userStore().setAuthoritys(res.data.data.authoritys)
            hasRoutes = true
            userStore().changeRouteStatus(hasRoutes)
        })
    }
    next()
})

4、实现动态导航

在这里插入图片描述
在这里插入图片描述

<template>
  <div>
    <a-tabs v-model:activeKey="activeKey" type="editable-card" @edit="onEdit">
      <a-tab-pane v-for="pane in panes" :key="pane.key" :tab="pane.title" :closable="pane.closable">
      </a-tab-pane>
    </a-tabs>
  </div>
</template>

<script setup lang="ts">
/*
*
* <close-outlined />
* */
import {
    
     defineComponent, ref,onMounted } from 'vue'
const panes = ref<{
      
       title: string; content: string; key: string; closable?: boolean }[]>([
  {
    
     title: 'Tab 1', content: 'Content of Tab 1', key: '1' },
  {
    
     title: 'Tab 2', content: 'Content of Tab 2', key: '2' },
  {
    
     title: 'Tab 3', content: 'Content of Tab 3', key: '3', closable: false },
]);
const activeKey = ref(panes.value[0].key);
const newTabIndex = ref(0);
onMounted(()=>{
    
    
})
const add = () => {
    
    
  activeKey.value = `newTab${
      
      ++newTabIndex.value}`;
  panes.value.length = 1
};
const remove = (targetKey: string) => {
    
    
  let lastIndex = 0;
  panes.value.forEach((pane, i) => {
    
    
    if (pane.key === targetKey) {
    
    
      lastIndex = i - 1;
    }
  });
  panes.value = panes.value.filter(pane => pane.key !== targetKey);
  if (panes.value.length && activeKey.value === targetKey) {
    
    
    if (lastIndex >= 0) {
    
    
      activeKey.value = panes.value[lastIndex].key;
    } else {
    
    
      activeKey.value = panes.value[0].key;
    }
  }
};

const onEdit = (targetKey: string | MouseEvent, action: string) => {
    
    
  if (action === 'add') {
    
    
    add();
  } else {
    
    
    remove(targetKey as string);
  }
};
</script>
<style scoped>
::v-deep .ant-tabs-nav-list .ant-tabs-nav-add span {
    
    
   transform: rotate(-45deg);
}
</style>

查看效果
在Home当中引入该内容
在这里插入图片描述

<div style="margin-top: 15px;">
   <Tabs></Tabs>
</div>
import Tabs from "@/views/inc/Tabs.vue"

在这里插入图片描述

5、设置侧栏和页面进行动态绑定

  • 在store.ts当中设置添加tab 的功能
    在这里插入图片描述
			 editableTabsValue: 0,
            editableTabs: [
                {
    
    
                    title: '首页',
                    content: '/index',
                    key: 0,
                    closable: false,
                }
            ],

		 getEditableTabsValue: (state) => state.editableTabsValue,
         getEditableTabs: (state) => state.editableTabs,

在这里插入图片描述

addTab(tab:any) {
    
    
            this.editableTabs.push({
    
    
                title: tab.title,
                content: tab.path,
                key: tab.key,
                closable: true,
            });
        },
        setEditableTabs(tab:any){
    
    
            this.editableTabs = tab;
        },
        setEditableTabsIndex0(){
    
    
            this.editableTabsValue = 0;
        },
        setEditableTabsIndexClearALL(){
    
    
            this.editableTabs  = [
                {
    
    
                    title: '首页',
                    content: '/index',
                    key: 0,
                    closable: false,
                }
            ]
        },
        setEditableTabsValue(tabValue:number){
    
    
            this.editableTabsValue = tabValue;
        }

在这里插入图片描述

 <a-menu-item key="1" @click="selectMenuIndex0">
        <template #icon>
          <MailOutlined />
        </template>
        主页
 </a-menu-item>
<a-menu-item :key="item.key" @click="selectMenu(item)">
          <template #icon>
            <Icon :icon-name="item.icon" />
          </template>
          {
    
    {
    
    item.title}}
 </a-menu-item>

在这里插入图片描述

onMounted(()=>{
    
    
  var menus = userStore().getEditableTabsValue;
  //设置高亮同步
  selectedKeys.value.length = 0;
  selectedKeys.value.push(menus+"")
  menuList.menus = userStore().getMenuList;
});
const selectMenu = (item:any) => {
    
    
  userStore().addTab(item)
}
const selectMenuIndex0 = () => {
    
    
  userStore().setEditableTabsIndex0()
}

6、完善Tabs标签页

在这里插入图片描述

<template>
  <div>
    <a-tabs v-model:activeKey="editableTabsValue" type="editable-card" @edit="onEdit">
      <a-tab-pane v-for="pane in editableTabs" :key="pane.key" :tab="pane.title" :closable="pane.closable">
      </a-tab-pane>
    </a-tabs>
  </div>
</template>

<script setup lang="ts">
import {
    
     userStore} from '@/store/store'
import {
    
    ref, onMounted,computed} from 'vue'

let editableTabs = computed({
    
    
  get(){
    
    
    return userStore().getEditableTabs;
  },
  set(val){
    
    
    userStore().addTab(val);
  }
});
let editableTabsValue = computed({
    
    
  get(){
    
    
    return userStore().getEditableTabsValue;
  },
  set(val:number){
    
    
    userStore().setEditableTabsValue(val);
  }
});
// @ts-ignore
const activeKey = ref(editableTabs.value[0].key);
const newTabIndex = ref(0);
let panesList = ref();
onMounted(()=>{
    
    
  panesList.value = userStore().getEditableTabs;
})
const removeAll = () => {
    
    
  activeKey.value = `newTab${
      
      ++newTabIndex.value}`;
  userStore().setEditableTabsIndexClearALL()
};
const remove = (targetKey: string) => {
    
    
  let lastIndex = 0;
  let uStore = userStore().getEditableTabs;
  uStore.forEach((pane, i) => {
    
    
    // @ts-ignore
    if (uStore.key === targetKey) {
    
    
      lastIndex = i - 1;
    }
  });
  // @ts-ignore
  uStore = uStore.filter(pane => pane.key !== targetKey);
  if (uStore.length && activeKey.value === targetKey) {
    
    
    if (lastIndex >= 0) {
    
    
      // @ts-ignore
      activeKey.value = uStore[lastIndex].key;
    } else {
    
    
      // @ts-ignore
      activeKey.value = uStore[0].key;
    }
  }
  userStore().setEditableTabs(uStore);
};
const onEdit = (targetKey: string | MouseEvent, action: string) => {
    
    
  if (action === 'add') {
    
    
    removeAll();
  } else {
    
    
    remove(targetKey as string);
  }
};
</script>
<style scoped>
::v-deep .ant-tabs-nav-list .ant-tabs-nav-add span {
    
    
   transform: rotate(-45deg);
}
</style>

  • 我们发现重复点击会重复的添加到上面,现在设置重复点击不会出现重复的信息
    在这里插入图片描述
    在这里插入图片描述
		addTab(tab:any) {
    
    
            let index = this.editableTabs.findIndex(e => e.title === tab.title )
            if(index === -1){
    
    
                this.editableTabs.push({
    
    
                    title: tab.title,
                    content: tab.path,
                    key: tab.key,
                    closable: true,
                });
            }
            this.editableTabsValue = tab.key;
        },

多次点击以后不会出现
在这里插入图片描述

  • 设置点击tab进行内容的切换
    在这里插入图片描述
<a-tabs v-model:activeKey="editableTabsValue" type="editable-card" @edit="onEdit" @tabClick="onTabClick">
      <a-tab-pane v-for="pane in editableTabs" :key="pane.key" :tab="pane.title" :closable="pane.closable">
      </a-tab-pane>
</a-tabs>

在这里插入图片描述

const onTabClick = (targetKey: string) => {
    
    
  let jsonArray = userStore().getEditableTabs
  let path = "";
  for(let i =0;i < jsonArray.length;i++){
    
    
    if(targetKey == jsonArray[i].key){
    
    
      path = jsonArray[i].content;
    }
  }
  userStore().setEditableTabsValue(targetKey)
  router.push(path);
}

在这里插入图片描述

 <div style="display: none"> {
    
    {
    
    editableTabsValue}}</div>
        后台管理系统
      </div>

在这里插入图片描述

let editableTabsValue = computed({
    
    
  get(){
    
    
    let key = userStore().getEditableTabsValue;
    selectedKeys.value.length = 0;
    selectedKeys.value.push(key)
    return key;
  },
  set(val:any){
    
    
    userStore().setMenuList(val);
  }
});

在这里插入图片描述
完善清除功能
在这里插入图片描述
在这里插入图片描述

const removeAll = () => {
    
    
  activeKey.value = `newTab${
      
      ++newTabIndex.value}`;
  userStore().setEditableTabsIndexClearALL()
  userStore().setEditableTabsValue("1")
  router.push("/index");
};

设置通过ip路径访问的时候,设置对应tabs和menu

在这里插入图片描述

<template>
  <router-view></router-view>
</template>
<script setup lang="ts">
import {
    
    ref,reactive,watch} from "vue"
import {
    
    useRouter} from "vue-router";
import {
    
    userStore} from "@/store/store";
const router = useRouter();
watch(
    () => router.currentRoute.value,
    (newValue, oldValue) => {
    
    
      let uStore = userStore().getEditableTabs;
      uStore.forEach((pane) => {
    
    
        if(pane.content === newValue.fullPath ){
    
    
          userStore().setEditableTabsValue(pane.key)
          router.push(pane.content);
        }
      });
    },
    {
    
     immediate: true }
)
</script>
<style scoped>
</style>

访问:http://localhost/#/sys/menus

在这里插入图片描述
设置退出登录后清除tab
在这里插入图片描述

 <a-menu-item key="setting:2" @click.native="logOut">退出</a-menu-item>

在这里插入图片描述

const logOut = () => {
    
    
  logout().then(response => {
    
    
    user.resetState()
    user.setEditableTabsIndexClearALL()
    user.setEditableTabsIndex0()
    localStorage.clear();
    sessionStorage.clear();
    router.push("/login");
  });
}

(四)菜单管理界面开发

1、在Menu当中设置表格样式

在这里插入图片描述

<template>
  <div>
    <a-form
        ref="formRef"
        :model="formState"
        :rules="rules"
    >
      <a-form-item :wrapper-col="{ span:24 }">
        <a-button type="primary" @click="onSubmit">新建</a-button>
      </a-form-item>
    </a-form>
    <a-table :columns="columns" :data-source="data" :row-selection="rowSelection">
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'type'">
          <a-tag color="blue" v-if="record.type == '目录'">{
    
    {
    
     record.type }}</a-tag>
          <a-tag color="green" v-if="record.type == '菜单'">{
    
    {
    
     record.type }}</a-tag>
          <a-tag color="orange" v-if="record.type == '按钮'">{
    
    {
    
     record.type }}</a-tag>
        </template>
        <template v-if="column.key === 'statu'">
          <a-tag color="green" v-if="record.statu == '正常'">{
    
    {
    
     record.statu }}</a-tag>
          <a-tag color="red" v-if="record.statu == '禁用'">{
    
    {
    
     record.statu }}</a-tag>
        </template>
        <template v-if="column.key === 'operation'">
          <a-button type="text" size="small" style="color: blue">
            编辑
          </a-button>
          <a-button type="text" size="small" style="color: red">
            删除
          </a-button>
        </template>
      </template>
    </a-table>
  </div>
</template>

<script name="menu" lang="ts" setup>
import {
    
     defineComponent,ref,reactive,toRaw } from 'vue';
import {
    
     SearchOutlined } from '@ant-design/icons-vue';
const formRef = ref();
const formState = reactive({
    
    
  name: undefined,
  sub: {
    
     name: undefined },
});
const rules = {
    
    
  parentId: {
    
    
    required: true,
    message: '请选择上级菜单',
  },
  name: {
    
    
    required: true,
    message: '请输入您的姓名',
  },
  perms: {
    
    
    required: true,
    message: '请输入权限编码',
  },
  type: {
    
    
    required: true,
    message: '请选择类型',
  },
  orderNum: {
    
    
    required: true,
    message: '请填入排序号',
  },
  statu: {
    
    
    required: true,
    message: '请选择状态',
  },
};
const onSubmit = () => {
    
    
  formRef.value.validate().then(() => {
    
    
        console.log('values', formState, toRaw(formState));
  }).catch(error => {
    
    
        console.log('error', error);
  });
};
const resetForm = () => {
    
    
  formRef.value.resetFields();
};
const columns = [
  {
    
    
    title: '名称',
    dataIndex: 'name',
    key: 'name',
  },
  {
    
    
    title: '权限编码',
    dataIndex: 'code',
    key: 'code',
  },
  {
    
    
    title: '图标',
    dataIndex: 'icon',
    key: 'icon',
  },
  {
    
    
    title: '类型',
    dataIndex: 'type',
    key: 'type',
  },
  {
    
    
    title: '菜单path',
    dataIndex: 'path',
    key: 'path',
  },
  {
    
    
    title: '菜单组件',
    dataIndex: 'component',
    key: 'component',
  },
  {
    
    
    title: '排序号',
    dataIndex: 'sort',
    key: 'sort',
  },{
    
    
    title: '状态',
    dataIndex: 'statu',
    key: 'statu',
  },
  {
    
    
    title: '操作',
    dataIndex: 'operation',
    key: 'operation',
  },
];

interface DataItem {
    
    
  key: number;
  name: string;
  code: string;
  sort: string;
  icon:string;
  statu:string;
  type: string;
  path: string;
  component: string;
  operation: string;
  children?: DataItem[];
}

const data: DataItem[] = [
  {
    
    
    key: 1,
    name: '系统管理',
    code: 'sys:system:list',
    type: "目录",
    path: "/",
    component: "/",
    sort: '1',
    icon: 'step-forward-outlined',
    statu: '正常',
    operation: '操作',
    children: [
      {
    
    
        key: 12,
        name: '用户管理',
        code: 'sys:user:list',
        type: "菜单",
        path: "/sys/user/list",
        component: "sys/User",
        sort: '2',
        icon: 'swap-right-outlined',
        statu: '正常',
        operation: '操作',
        children: [
          {
    
    
            key: 121,
            name: '查询',
            code: 'sys:user:list',
            type: "按钮",
            path: "",
            component: "",
            sort: '3',
            icon: 'swap-right-outlined',
            statu: '禁用',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '新增',
            code: 'sys:user:add',
            type: "按钮",
            path: "",
            component: "",
            sort: '4',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '修改',
            code: 'sys:user:edit',
            type: "按钮",
            path: "",
            component: "",
            sort: '5',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '删除',
            code: 'sys:user:delete',
            type: "按钮",
            path: "",
            component: "",
            sort: '6',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '重置密码',
            code: 'sys:user:repass',
            type: "按钮",
            path: "",
            component: "",
            sort: '7',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
        ],
      },
      {
    
    
        key: 122,
        name: '角色管理',
        code: 'sys:role:list',
        type: "目录",
        path: "/sys/role/list",
        component: "sys/Role",
        sort: '8',
        icon: 'step-forward-outlined',
        statu: '正常',
        operation: '操作',
        children: [
          {
    
    
            key: 1212,
            name: '查询',
            code: 'sys:role:list',
            type: "菜单",
            path: "",
            component: "",
            sort: '9',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1213,
            name: '新增',
            code: 'sys:role:add',
            type: "菜单",
            path: "",
            component: "",
            sort: '10',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1214,
            name: '修改',
            code: 'sys:role:edit',
            type: "菜单",
            path: "",
            component: "",
            sort: '11',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1215,
            name: '删除',
            code: 'sys:role:delete',
            type: "菜单",
            path: "",
            component: "",
            sort: '12',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '重置密码',
            code: 'sys:user:repass',
            type: "按钮",
            path: "",
            component: "",
            sort: '7',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
        ],
      },
    ],
  },
];

const rowSelection = {
    
    
  onChange: (selectedRowKeys: (string | number)[], selectedRows: DataItem[]) => {
    
    
    console.log(`selectedRowKeys: ${
      
      selectedRowKeys}`, 'selectedRows: ', selectedRows);
  },
  onSelect: (record: DataItem, selected: boolean, selectedRows: DataItem[]) => {
    
    
    console.log(record, selected, selectedRows);
  },
  onSelectAll: (selected: boolean, selectedRows: DataItem[], changeRows: DataItem[]) => {
    
    
    console.log(selected, selectedRows, changeRows);
  },
};

</script>

<style scoped>

</style>

在这里插入图片描述

上面当面数据超出页面的时候页面跟着滚动这样不太好我们优化一下,设置侧边栏和头部不懂,设置内容区域滚动
在这里插入图片描述
在这里插入图片描述

<template>
  <a-layout has-sider :style="{ position: 'fixed', zIndex: 1, width: '100%' }">
    <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible >
      <div class="logo" />
      <SideMenu></SideMenu>
    </a-layout-sider><a-layout-content :style="{  marginTop: '0' }">
          <a-menu
              v-model:selectedKeys="selectedKeysTop"
              theme="dark"
              mode="horizontal"
              :style="{ lineHeight: '64px',marginLeft:'-15px' }"
          >
            <a-sub-menu key="sub2" >
              <template #title>
                <div >
                  <menu-unfold-outlined
                      v-if="collapsed"
                      class="trigger"
                      @click="() => (collapsed = !collapsed)"
                  />
                  <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
                </div>
              </template>
            </a-sub-menu>
            <a-sub-menu key="sub1" style="margin-left: 85%" >
              <template #title>
                <a-avatar v-if="userInfo.avatar == null || userInfo.avatar == ''" style="background-color: #87d068">
                  <template #icon>
                    <UserOutlined />
                  </template>
                </a-avatar>
                <a-avatar style="margin-top: -10px" v-if="userInfo.avatar != null && userInfo.avatar != ''"  src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
                <a class="ant-dropdown-link" @click.prevent>
                  <view style="margin-top: 10%;margin-left: 10%">
                    {
    
    {
    
    userInfo.username}}
                  </view>
                  <DownOutlined />
                </a>
              </template>
              <a-menu-item key="setting:1">
                <router-link to="/userCenter">
                  个人中心
                </router-link>
              </a-menu-item>
              <a-menu-item key="setting:2" @click.native="logOut">退出</a-menu-item>
            </a-sub-menu>
          </a-menu>
          <div style="margin-top: 15px;">
            <Tabs></Tabs>
          </div>
          <div id="components-affix-demo-target" ref="containerRef" class="scrollable-container" :style="{ background: 'rgb(255,255,255)', padding: '15px', minHeight: '820px' }">
            <router-view>
            </router-view>
          </div>
    </a-layout-content>
  </a-layout>
</template>
<script lang="ts" setup>
import {
    
    
  DownOutlined,
  UserOutlined,
  MenuUnfoldOutlined,
  MenuFoldOutlined,
} from '@ant-design/icons-vue';
import {
    
     userStore} from '@/store/store'
import {
    
     useRoute, useRouter } from "vue-router";
import SideMenu from "@/views/inc/SideMenu.vue"
import Tabs from "@/views/inc/Tabs.vue"
import {
    
     ref,reactive } from 'vue';
import {
    
     getUserInfo,logout } from "@/api";
const user = userStore()
let selectedKeys =ref<string[]>(['1'])
let selectedKeysTop =ref<string[]>(['1'])
let collapsed =ref<boolean>(false)
let gridInfo = ref('')
// 获取路由信息
const router = useRouter();
let userInfo = reactive({
    
    
  id: '',
  username: 'admin',
  avatar: '',
});
getUserInfo().then(response => {
    
    
  gridInfo.value = response.data.data
  userInfo = Object.assign(userInfo,gridInfo.value);
});
const logOut = () => {
    
    
  logout().then(response => {
    
    
    user.resetState()
    user.setEditableTabsIndexClearALL()
    user.setEditableTabsIndex0()
    localStorage.clear();
    sessionStorage.clear();
    router.push("/login");
  });
}
</script>
<style>
#components-layout-demo-custom-trigger .trigger {
    
    
  font-size: 18px;
  line-height: 64px;
  padding: 0 24px;
  cursor: pointer;
  transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
    
    
  color: #fb7299;
}
#components-layout-demo-custom-trigger .logo {
    
    
  height: 32px;
  background: rgba(255, 255, 255, 0.3);
  margin: 16px;
}
.site-layout .site-layout-background {
    
    
  background: #fff;
}
#components-affix-demo-target.scrollable-container {
    
    
  height: 100px;
  overflow-y: scroll;
}
#components-affix-demo-target .background {
    
    
  padding-top: 60px;
  height: 300px;
}
</style>

内容滚动头部底部不滚
在这里插入图片描述

2、在Menu当中设置新增和编辑

在这里插入图片描述
在这里插入图片描述

<template>
  <div>
    <a-row>
      <a-col :span="2">
        <a-button @click="iconValue = 'step-backward-outlined'" >
          <template #icon>
            <step-backward-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'step-forward-outlined'" >
          <template #icon>
            <step-forward-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'fast-backward-outlined'" >
          <template #icon>
            <fast-backward-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'fast-forward-outlined'" >
          <template #icon>
            <fast-forward-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'shrink-outlined'" >
          <template #icon>
            <shrink-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'arrows-alt-outlined'" >
          <template #icon>
            <arrows-alt-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'down-outlined'" >
          <template #icon>
            <down-outlined />
          </template>
        </a-button>
      </a-col>


      <a-col :span="2">
        <a-button  @click="iconValue = 'up-outlined'" >
          <template #icon>
            <up-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'left-outlined'" >
          <template #icon>
            <left-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'right-outlined'" >
          <template #icon>
            <right-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'caret-up-outlined'" >
          <template #icon>
            <caret-up-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'caret-down-outlined'" >
          <template #icon>
            <caret-down-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'caret-left-outlined'" >
          <template #icon>
            <caret-left-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'caret-right-outlined'" >
          <template #icon>
            <caret-right-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'up-circle-outlined'" >
          <template #icon>
            <up-circle-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'down-circle-outlined'" >
          <template #icon>
            <down-circle-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'down-circle-outlined'" >
          <template #icon>
            <down-circle-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'left-circle-outlined'" >
          <template #icon>
            <left-circle-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'right-circle-outlined'" >
          <template #icon>
            <right-circle-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'right-circle-outlined'" >
          <template #icon>
            <right-circle-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'double-left-outlined'" >
          <template #icon>
            <double-left-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'vertical-left-outlined'" >
          <template #icon>
            <vertical-left-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'vertical-right-outlined'" >
          <template #icon>
            <vertical-right-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'vertical-align-top-outlined'" >
          <template #icon>
            <vertical-align-top-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'vertical-align-middle-outlined'" >
          <template #icon>
            <vertical-align-middle-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'vertical-align-middle-outlined'" >
          <template #icon>
            <vertical-align-middle-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'vertical-align-bottom-outlined'" >
          <template #icon>
            <vertical-align-bottom-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'forward-outlined'" >
          <template #icon>
            <forward-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'backward-outlined'" >
          <template #icon>
            <backward-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'rollback-outlined'" >
          <template #icon>
            <rollback-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'enter-outlined'" >
          <template #icon>
            <enter-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button   @click="iconValue = 'retweet-outlined'" >
          <template #icon>
            <retweet-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'step-backward-outlined'" >
          <template #icon>
            <menu-fold-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'menu-unfold-outlined'" >
          <template #icon>
            <menu-unfold-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'align-center-outlined'" >
          <template #icon>
            <align-center-outlined />
          </template>
        </a-button>
      </a-col><a-col :span="2">
      <a-button  @click="iconValue = 'align-left-outlined'" >
        <template #icon>
          <align-left-outlined />
        </template>
      </a-button>
    </a-col><a-col :span="2">
      <a-button  @click="iconValue = 'ordered-list-outlined'" >
        <template #icon>
          <ordered-list-outlined />
        </template>
      </a-button>
    </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'unordered-list-outlined'" >
          <template #icon>
            <unordered-list-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'appstore-outlined'" >
          <template #icon>
            <appstore-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'bars-outlined'" >
          <template #icon>
            <bars-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'vertical-align-top-outlined'" >
          <template #icon>
            <vertical-align-top-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'bulb-outlined'" >
          <template #icon>
            <bulb-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'console-sql-outlined'" >
          <template #icon>
            <console-sql-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'desktop-outlined'" >
          <template #icon>
            <desktop-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button   @click="iconValue = 'vertical-align-top-outlined'" >
          <template #icon>
            <vertical-align-top-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button   @click="iconValue = 'vertical-align-top-outlined'" >
          <template #icon>
            <vertical-align-top-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'exception-outline'" >
          <template #icon>
            <exception-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'file-word-outlined'" >
          <template #icon>
            <file-word-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button  @click="iconValue = 'file-markdown-outlined'" >
          <template #icon>
            <file-markdown-outlined />
          </template>
        </a-button>
      </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'file-search-outlined'" >
          <template #icon>
            <file-search-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button   @click="iconValue = 'file-protect-outlined'" >
          <template #icon>
            <file-protect-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'hdd-outlined'" >
          <template #icon>
            <hdd-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button   @click="iconValue = 'insert-row-left-outlined'" >
          <template #icon>
            <insert-row-left-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button   @click="iconValue = 'merge-cells-outlined'" >
          <template #icon>
            <merge-cells-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'printer-outlined'" >
          <template #icon>
            <printer-outlined />
          </template>
        </a-button>
      </a-col><a-col :span="2">
      <a-button  @click="iconValue = 'reconciliation-outlined'" >
        <template #icon>
          <reconciliation-outlined />
        </template>
      </a-button>
    </a-col><a-col :span="2">
      <a-button  @click="iconValue = 'shop-outlined'" >
        <template #icon>
          <shop-outlined />
        </template>
      </a-button>
    </a-col>

      <a-col :span="2">
        <a-button   @click="iconValue = 'split-cells-outlined'" >
          <template #icon>
            <split-cells-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'usergroup-add-outlined'" >
          <template #icon>
            <usergroup-add-outlined />
          </template>
        </a-button>
      </a-col>
      <a-col :span="2">
        <a-button  @click="iconValue = 'woman-outlined'" >
          <template #icon>
            <woman-outlined />
          </template>
        </a-button>
      </a-col>
    </a-row>
  </div>
</template>


<script setup lang="ts">
import {
    
    ref,defineExpose} from "vue";
let iconValue = ref('')
const props = defineProps<{
      
      
  iconName: any;
}>();

const change = () => {
    
    
  console.log(iconValue.value)
}

defineExpose({
    
    
  change,
  iconValue
})
</script>

<style scoped>

</style>

在这里插入图片描述

<template>
  <div>
    <a-form
        ref="formRef"
        :model="formState"
        :rules="rules"
    >
      <a-form-item :wrapper-col="{ span:24 }">
        <a-button type="primary" @click="showDrawer">新建</a-button>
      </a-form-item>
    </a-form>
    <a-table :columns="columns" :data-source="data" :row-selection="rowSelection">
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'type'">
          <a-tag color="blue" v-if="record.type == '目录'">{
    
    {
    
     record.type }}</a-tag>
          <a-tag color="green" v-if="record.type == '菜单'">{
    
    {
    
     record.type }}</a-tag>
          <a-tag color="orange" v-if="record.type == '按钮'">{
    
    {
    
     record.type }}</a-tag>
        </template>
        <template v-if="column.key === 'statu'">
          <a-tag color="green" v-if="record.statu == '正常'">{
    
    {
    
     record.statu }}</a-tag>
          <a-tag color="red" v-if="record.statu == '禁用'">{
    
    {
    
     record.statu }}</a-tag>
        </template>
        <template v-if="column.key === 'operation'">
          <a-button type="text" size="small" style="color: blue">
            编辑
          </a-button>
          <a-button type="text" size="small" style="color: red">
            删除
          </a-button>
        </template>
      </template>
    </a-table>
    <a-drawer
        title="添加菜单"
        :width="600"
        :visible="visible"
        :body-style="{ paddingBottom: '80px' }"
        :footer-style="{ textAlign: 'right' }"
        @close="onClose"
    >
       <a-form
          ref="formRef"
          name="custom-validation"
          :model="formState"
          :rules="rules"
          v-bind="layout"
          @finish="handleFinish"
          @validate="handleValidate"
          @finishFailed="handleFinishFailed"
      >
        <a-form-item has-feedback label="上级菜单" name="parentId">
          <a-input-group compact>
            <a-cascader
                v-model:value="formState.parentId"
                :options="options"
                type="parentId"
                placeholder="选择上级菜单"
            />
          </a-input-group>
        </a-form-item>
        <a-form-item has-feedback label="菜单名称" name="name">
          <a-input v-model:value="formState.name" type="name" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="权限编码" name="perms">
          <a-input v-model:value="formState.perms" type="perms" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="图标" name="icon">
          <a-input  v-model:value="formState.icon" @click="showModal" type="icon" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="菜单URL" name="path">
          <a-input v-model:value="formState.path" type="path" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="菜单组件" name="component">
          <a-input v-model:value="formState.component" type="component" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="类型" name="type" >
          <a-checkbox-group v-model:value="formState.type">
            <a-checkbox value="1" name="type">目录</a-checkbox>
            <a-checkbox value="2" name="type">菜单</a-checkbox>
            <a-checkbox value="3" name="type">按钮</a-checkbox>
          </a-checkbox-group>
        </a-form-item>
        <a-form-item has-feedback label="状态" name="statu">
          <a-checkbox-group v-model:value="formState.statu">
            <a-checkbox value="1" name="type">禁用</a-checkbox>
            <a-checkbox value="2" name="type">正常</a-checkbox>
          </a-checkbox-group>
        </a-form-item>
        <a-form-item has-feedback label="排序" name="orderNum">
          <a-input-number v-model:value="formState.orderNum" />
        </a-form-item>
        <a-form-item :wrapper-col="{ span: 20, offset: 4 }">
          <a-button type="primary" html-type="submit">提交</a-button>
          <a-button style="margin-left: 10px" @click="resetForm">重置</a-button>
        </a-form-item>
      </a-form>
    </a-drawer>
    <a-modal v-model:visible="visibleIcon" title="选择图标" @ok="handleOk">
      <IconTable ref="myIcons"></IconTable>
    </a-modal>
  </div>
</template>

<script name="menu" lang="ts" setup>
import {
    
     defineComponent,ref,reactive,toRaw } from 'vue';
import {
    
     SearchOutlined } from '@ant-design/icons-vue';
import type {
    
     DrawerProps } from 'ant-design-vue';
import type {
    
     Rule } from 'ant-design-vue/es/form';
import type {
    
     FormInstance } from 'ant-design-vue';

import IconTable from  '@/components/IconTable.vue'
//获取绑定的ref
const myIcons = ref();
const formRef = ref<FormInstance>();
const placement = ref<DrawerProps['placement']>('right');
const visible = ref<boolean>(false);
const value18 = ref<string[]>([]);
interface FormState {
    
    
  parentId: string;
  name: string;
  perms: string;
  icon: string;
  path: string;
  component: string;
  type: string;
  statu: number | undefined;
  orderNum: number | undefined;
}
const formState = reactive<FormState>({
    
    
  parentId:  '',
  name: '',
  perms:  '',
  icon: '',
  path:  '',
  component:  '',
  type: '',
  statu: 0,
  orderNum:  0,
});
const options = [
  {
    
    
    value: '主页',
    label: '主页',
  },
  {
    
    
    value: '系统管理',
    label: '系统管理',
    children: [
      {
    
    
        value: '用户管理',
        label: '用户管理'
      },
      {
    
    
        value: '角色管理',
        label: '角色管理'
      },
      {
    
    
        value: '菜单管理',
        label: '菜单管理'
      },
    ],
  },
  {
    
    
    value: '系统工具',
    label: '系统工具',
    children: [
      {
    
    
        value: '数据字典',
        label: '数据字典',
      },
    ],
  },
]


let checkName= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入菜单名称');
  }
};

let checkPath= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入路径');
  }
};


let checkParentId= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择父目录');
  }
};

let checkPerms= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入权限编码');
  }
};


let checkIcon= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择图标');
  }
};



let checkComponent= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入组件');
  }
};

let checkType= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择类型');
  }
};

let checkStatu= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择状态');
  }
};


let checkOrderNum= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入排序');
  }
};




let checkAge = async (_rule: Rule, value: number) => {
    
    
  if (!value) {
    
    
    return Promise.reject('Please input the age');
  }
  if (!Number.isInteger(value)) {
    
    
    return Promise.reject('Please input digits');
  } else {
    
    
    if (value < 18) {
    
    
      return Promise.reject('Age must be greater than 18');
    } else {
    
    
      return Promise.resolve();
    }
  }
};

let validatePass = async (_rule: Rule, value: string) => {
    
    
  if (value === '') {
    
    
    return Promise.reject('Please input the password');
  } else {
    
    
    // @ts-ignore
    if (formState.checkPass !== '') {
    
    
      // @ts-ignore
      formRef.value.validateFields('checkPass');
    }
    return Promise.resolve();
  }
};
let validatePass2 = async (_rule: Rule, value: string) => {
    
    
  if (value === '') {
    
    
    return Promise.reject('Please input the password again');
    // @ts-ignore
  } else if (value !== formState.pass) {
    
    
    return Promise.reject("Two inputs don't match!");
  } else {
    
    
    return Promise.resolve();
  }
};
const rules: Record<string, Rule[]> = {
    
    
  pass: [{
    
     required: true, validator: validatePass, trigger: 'change' }],
  checkPass: [{
    
     validator: validatePass2, trigger: 'change' }],
  age: [{
    
     validator: checkAge, trigger: 'change' }],
  parentId: [{
    
     validator: checkParentId, trigger: 'change' }],
  name: [{
    
     validator: checkName, trigger: 'change' }],
  perms: [{
    
     validator: checkPerms, trigger: 'change' }],
  icon: [{
    
     validator: checkIcon, trigger: 'change' }],
  path: [{
    
     validator: checkPath, trigger: 'change' }],
  component: [{
    
     validator: checkComponent, trigger: 'change' }],
  type: [{
    
     validator: checkType, trigger: 'change' }],
  statu: [{
    
     validator: checkStatu, trigger: 'change' }],
  orderNum: [{
    
     validator: checkOrderNum, trigger: 'change' }],
};
const layout = {
    
    
  labelCol: {
    
     span: 4 },
  wrapperCol: {
    
     span: 14 },
};
const handleFinish = (values: FormState) => {
    
    
  console.log(values, formState);
};
const handleFinishFailed = (errors: any) => {
    
    
  console.log(errors);
};
const resetForm = () => {
    
    
  // @ts-ignore
  formRef.value.resetFields();
};
const handleValidate = (...args: any[]) => {
    
    
  console.log(args);
};

const showDrawer = () => {
    
    
  visible.value = true;
};
const onClose = () => {
    
    
  visible.value = false;
};
const visibleIcon = ref<boolean>(false);

const showModal = () => {
    
    
  visibleIcon.value = true;
};

const handleOk = (e: MouseEvent) => {
    
    
  console.log(e);
  visibleIcon.value = false;
  myIcons.value.change()
  //这里也可以通过ref获取到子组件暴露出来想要父组件获取到的值
  formState.icon = myIcons.value.iconValue
};
const columns = [
  {
    
    
    title: '名称',
    dataIndex: 'name',
    key: 'name',
  },
  {
    
    
    title: '权限编码',
    dataIndex: 'code',
    key: 'code',
  },
  {
    
    
    title: '图标',
    dataIndex: 'icon',
    key: 'icon',
  },
  {
    
    
    title: '类型',
    dataIndex: 'type',
    key: 'type',
  },
  {
    
    
    title: '菜单path',
    dataIndex: 'path',
    key: 'path',
  },
  {
    
    
    title: '菜单组件',
    dataIndex: 'component',
    key: 'component',
  },
  {
    
    
    title: '排序号',
    dataIndex: 'sort',
    key: 'sort',
  },{
    
    
    title: '状态',
    dataIndex: 'statu',
    key: 'statu',
  },
  {
    
    
    title: '操作',
    dataIndex: 'operation',
    key: 'operation',
  },
];

interface DataItem {
    
    
  key: number;
  name: string;
  code: string;
  sort: string;
  icon:string;
  statu:string;
  type: string;
  path: string;
  component: string;
  operation: string;
  children?: DataItem[];
}

const data: DataItem[] = [
  {
    
    
    key: 1,
    name: '系统管理',
    code: 'sys:system:list',
    type: "目录",
    path: "/",
    component: "/",
    sort: '1',
    icon: 'step-forward-outlined',
    statu: '正常',
    operation: '操作',
    children: [
      {
    
    
        key: 12,
        name: '用户管理',
        code: 'sys:user:list',
        type: "菜单",
        path: "/sys/user/list",
        component: "sys/User",
        sort: '2',
        icon: 'swap-right-outlined',
        statu: '正常',
        operation: '操作',
        children: [
          {
    
    
            key: 121,
            name: '查询',
            code: 'sys:user:list',
            type: "按钮",
            path: "",
            component: "",
            sort: '3',
            icon: 'swap-right-outlined',
            statu: '禁用',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '新增',
            code: 'sys:user:add',
            type: "按钮",
            path: "",
            component: "",
            sort: '4',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '修改',
            code: 'sys:user:edit',
            type: "按钮",
            path: "",
            component: "",
            sort: '5',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '删除',
            code: 'sys:user:delete',
            type: "按钮",
            path: "",
            component: "",
            sort: '6',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '重置密码',
            code: 'sys:user:repass',
            type: "按钮",
            path: "",
            component: "",
            sort: '7',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
        ],
      },
      {
    
    
        key: 122,
        name: '角色管理',
        code: 'sys:role:list',
        type: "目录",
        path: "/sys/role/list",
        component: "sys/Role",
        sort: '8',
        icon: 'step-forward-outlined',
        statu: '正常',
        operation: '操作',
        children: [
          {
    
    
            key: 1212,
            name: '查询',
            code: 'sys:role:list',
            type: "菜单",
            path: "",
            component: "",
            sort: '9',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1213,
            name: '新增',
            code: 'sys:role:add',
            type: "菜单",
            path: "",
            component: "",
            sort: '10',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1214,
            name: '修改',
            code: 'sys:role:edit',
            type: "菜单",
            path: "",
            component: "",
            sort: '11',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1215,
            name: '删除',
            code: 'sys:role:delete',
            type: "菜单",
            path: "",
            component: "",
            sort: '12',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '重置密码',
            code: 'sys:user:repass',
            type: "按钮",
            path: "",
            component: "",
            sort: '13',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
        ],
      },
    ],
  },
];

const rowSelection = {
    
    
  onChange: (selectedRowKeys: (string | number)[], selectedRows: DataItem[]) => {
    
    
    console.log(`selectedRowKeys: ${
      
      selectedRowKeys}`, 'selectedRows: ', selectedRows);
  },
  onSelect: (record: DataItem, selected: boolean, selectedRows: DataItem[]) => {
    
    
    console.log(record, selected, selectedRows);
  },
  onSelectAll: (selected: boolean, selectedRows: DataItem[], changeRows: DataItem[]) => {
    
    
    console.log(selected, selectedRows, changeRows);
  },
};

</script>

<style scoped>

</style>

在这里插入图片描述

(五)角色管理

1、设置角色信息的增删改查-权限分配

在这里插入图片描述

<template>
  <div>
    <a-form
        ref="formRef"
        :model="formState"
        :rules="rules"
    >
      <a-form-item :wrapper-col="{ span:24 }">
        <a-button type="primary" @click="showDrawer">新建</a-button>
      </a-form-item>
    </a-form>
    <a-table :columns="columns" :data-source="data" :row-selection="rowSelection">
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'type'">
          <a-tag color="blue" v-if="record.type == '目录'">{
    
    {
    
     record.type }}</a-tag>
          <a-tag color="green" v-if="record.type == '菜单'">{
    
    {
    
     record.type }}</a-tag>
          <a-tag color="orange" v-if="record.type == '按钮'">{
    
    {
    
     record.type }}</a-tag>
        </template>
        <template v-if="column.key === 'statu'">
          <a-tag color="green" v-if="record.statu == '正常'">{
    
    {
    
     record.statu }}</a-tag>
          <a-tag color="red" v-if="record.statu == '禁用'">{
    
    {
    
     record.statu }}</a-tag>
        </template>
        <template v-if="column.key === 'operation'">
          <a-button type="text" size="small" style="color: blue">
            编辑
          </a-button>
          <a-button type="text" size="small" style="color: red">
            删除
          </a-button>
        </template>
      </template>
    </a-table>
    <a-drawer
        title="添加菜单"
        :width="600"
        :visible="visible"
        :body-style="{ paddingBottom: '80px' }"
        :footer-style="{ textAlign: 'right' }"
        @close="onClose"
    >
       <a-form
          ref="formRef"
          name="custom-validation"
          :model="formState"
          :rules="rules"
          v-bind="layout"
          @finish="handleFinish"
          @validate="handleValidate"
          @finishFailed="handleFinishFailed"
      >
        <a-form-item has-feedback label="上级菜单" name="parentId">
          <a-input-group compact>
            <a-cascader
                v-model:value="formState.parentId"
                :options="options"
                type="parentId"
                placeholder="选择上级菜单"
            />
          </a-input-group>
        </a-form-item>
        <a-form-item has-feedback label="菜单名称" name="name">
          <a-input v-model:value="formState.name" type="name" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="权限编码" name="perms">
          <a-input v-model:value="formState.perms" type="perms" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="图标" name="icon">
          <a-input  v-model:value="formState.icon" @click="showModal" type="icon" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="菜单URL" name="path">
          <a-input v-model:value="formState.path" type="path" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="菜单组件" name="component">
          <a-input v-model:value="formState.component" type="component" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="类型" name="type" >
          <a-checkbox-group v-model:value="formState.type">
            <a-checkbox value="1" name="type">目录</a-checkbox>
            <a-checkbox value="2" name="type">菜单</a-checkbox>
            <a-checkbox value="3" name="type">按钮</a-checkbox>
          </a-checkbox-group>
        </a-form-item>
        <a-form-item has-feedback label="状态" name="statu">
          <a-checkbox-group v-model:value="formState.statu">
            <a-checkbox value="1" name="type">禁用</a-checkbox>
            <a-checkbox value="2" name="type">正常</a-checkbox>
          </a-checkbox-group>
        </a-form-item>
        <a-form-item has-feedback label="排序" name="orderNum">
          <a-input-number v-model:value="formState.orderNum" />
        </a-form-item>
        <a-form-item :wrapper-col="{ span: 20, offset: 4 }">
          <a-button type="primary" html-type="submit">提交</a-button>
          <a-button style="margin-left: 10px" @click="resetForm">重置</a-button>
        </a-form-item>
      </a-form>
    </a-drawer>
    <a-modal v-model:visible="visibleIcon" title="选择图标" @ok="handleOk" okText="确认" cancelText="取消">
      <IconTable ref="myIcons"></IconTable>
    </a-modal>
  </div>
</template>

<script name="menu" lang="ts" setup>
import {
    
     defineComponent,ref,reactive,toRaw } from 'vue';
import {
    
     SearchOutlined } from '@ant-design/icons-vue';
import type {
    
     DrawerProps } from 'ant-design-vue';
import type {
    
     Rule } from 'ant-design-vue/es/form';
import type {
    
     FormInstance } from 'ant-design-vue';

import IconTable from  '@/components/IconTable.vue'
//获取绑定的ref
const myIcons = ref();
const formRef = ref<FormInstance>();
const placement = ref<DrawerProps['placement']>('right');
const visible = ref<boolean>(false);
const value18 = ref<string[]>([]);
interface FormState {
    
    
  parentId: string;
  name: string;
  perms: string;
  icon: string;
  path: string;
  component: string;
  type: string;
  statu: number | undefined;
  orderNum: number | undefined;
}
const formState = reactive<FormState>({
    
    
  parentId:  '',
  name: '',
  perms:  '',
  icon: '',
  path:  '',
  component:  '',
  type: '',
  statu: 0,
  orderNum:  0,
});

const options = [
  {
    
    
    value: '主页',
    label: '主页',
  },
  {
    
    
    value: '系统管理',
    label: '系统管理',
    children: [
      {
    
    
        value: '用户管理',
        label: '用户管理'
      },
      {
    
    
        value: '角色管理',
        label: '角色管理'
      },
      {
    
    
        value: '菜单管理',
        label: '菜单管理'
      },
    ],
  },
  {
    
    
    value: '系统工具',
    label: '系统工具',
    children: [
      {
    
    
        value: '数据字典',
        label: '数据字典',
      },
    ],
  },
]


let checkName= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入菜单名称');
  }
};

let checkPath= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入路径');
  }
};


let checkParentId= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择父目录');
  }
};

let checkPerms= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入权限编码');
  }
};


let checkIcon= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择图标');
  }
};



let checkComponent= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入组件');
  }
};

let checkType= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择类型');
  }
};

let checkStatu= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择状态');
  }
};


let checkOrderNum= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入排序');
  }
};




let checkAge = async (_rule: Rule, value: number) => {
    
    
  if (!value) {
    
    
    return Promise.reject('Please input the age');
  }
  if (!Number.isInteger(value)) {
    
    
    return Promise.reject('Please input digits');
  } else {
    
    
    if (value < 18) {
    
    
      return Promise.reject('Age must be greater than 18');
    } else {
    
    
      return Promise.resolve();
    }
  }
};

let validatePass = async (_rule: Rule, value: string) => {
    
    
  if (value === '') {
    
    
    return Promise.reject('Please input the password');
  } else {
    
    
    // @ts-ignore
    if (formState.checkPass !== '') {
    
    
      // @ts-ignore
      formRef.value.validateFields('checkPass');
    }
    return Promise.resolve();
  }
};
let validatePass2 = async (_rule: Rule, value: string) => {
    
    
  if (value === '') {
    
    
    return Promise.reject('Please input the password again');
    // @ts-ignore
  } else if (value !== formState.pass) {
    
    
    return Promise.reject("Two inputs don't match!");
  } else {
    
    
    return Promise.resolve();
  }
};
const rules: Record<string, Rule[]> = {
    
    
  pass: [{
    
     required: true, validator: validatePass, trigger: 'change' }],
  checkPass: [{
    
     validator: validatePass2, trigger: 'change' }],
  age: [{
    
     validator: checkAge, trigger: 'change' }],
  parentId: [{
    
     validator: checkParentId, trigger: 'change' }],
  name: [{
    
     validator: checkName, trigger: 'change' }],
  perms: [{
    
     validator: checkPerms, trigger: 'change' }],
  icon: [{
    
     validator: checkIcon, trigger: 'change' }],
  path: [{
    
     validator: checkPath, trigger: 'change' }],
  component: [{
    
     validator: checkComponent, trigger: 'change' }],
  type: [{
    
     validator: checkType, trigger: 'change' }],
  statu: [{
    
     validator: checkStatu, trigger: 'change' }],
  orderNum: [{
    
     validator: checkOrderNum, trigger: 'change' }],
};
const layout = {
    
    
  labelCol: {
    
     span: 4 },
  wrapperCol: {
    
     span: 20 },
};
const handleFinish = (values: FormState) => {
    
    
  console.log(values, formState);
};
const handleFinishFailed = (errors: any) => {
    
    
  console.log(errors);
};
const resetForm = () => {
    
    
  // @ts-ignore
  formRef.value.resetFields();
};
const handleValidate = (...args: any[]) => {
    
    
  console.log(args);
};

const showDrawer = () => {
    
    
  visible.value = true;
};
const onClose = () => {
    
    
  visible.value = false;
};
const visibleIcon = ref<boolean>(false);

const showModal = () => {
    
    
  visibleIcon.value = true;
};

const handleOk = (e: MouseEvent) => {
    
    
  console.log(e);
  visibleIcon.value = false;
  myIcons.value.change()
  //这里也可以通过ref获取到子组件暴露出来想要父组件获取到的值
  formState.icon = myIcons.value.iconValue
};
const columns = [
  {
    
    
    title: '名称',
    dataIndex: 'name',
    key: 'name',
  },
  {
    
    
    title: '权限编码',
    dataIndex: 'code',
    key: 'code',
  },
  {
    
    
    title: '图标',
    dataIndex: 'icon',
    key: 'icon',
  },
  {
    
    
    title: '类型',
    dataIndex: 'type',
    key: 'type',
  },
  {
    
    
    title: '菜单path',
    dataIndex: 'path',
    key: 'path',
  },
  {
    
    
    title: '菜单组件',
    dataIndex: 'component',
    key: 'component',
  },
  {
    
    
    title: '排序号',
    dataIndex: 'sort',
    key: 'sort',
  },{
    
    
    title: '状态',
    dataIndex: 'statu',
    key: 'statu',
  },
  {
    
    
    title: '操作',
    dataIndex: 'operation',
    key: 'operation',
  },
];

interface DataItem {
    
    
  key: number;
  name: string;
  code: string;
  sort: string;
  icon:string;
  statu:string;
  type: string;
  path: string;
  component: string;
  operation: string;
  children?: DataItem[];
}

const data: DataItem[] = [
  {
    
    
    key: 1,
    name: '系统管理',
    code: 'sys:system:list',
    type: "目录",
    path: "/",
    component: "/",
    sort: '1',
    icon: 'step-forward-outlined',
    statu: '正常',
    operation: '操作',
    children: [
      {
    
    
        key: 12,
        name: '用户管理',
        code: 'sys:user:list',
        type: "菜单",
        path: "/sys/user/list",
        component: "sys/User",
        sort: '2',
        icon: 'swap-right-outlined',
        statu: '正常',
        operation: '操作',
        children: [
          {
    
    
            key: 121,
            name: '查询',
            code: 'sys:user:list',
            type: "按钮",
            path: "",
            component: "",
            sort: '3',
            icon: 'swap-right-outlined',
            statu: '禁用',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '新增',
            code: 'sys:user:add',
            type: "按钮",
            path: "",
            component: "",
            sort: '4',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '修改',
            code: 'sys:user:edit',
            type: "按钮",
            path: "",
            component: "",
            sort: '5',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '删除',
            code: 'sys:user:delete',
            type: "按钮",
            path: "",
            component: "",
            sort: '6',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '重置密码',
            code: 'sys:user:repass',
            type: "按钮",
            path: "",
            component: "",
            sort: '7',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
        ],
      },
      {
    
    
        key: 122,
        name: '角色管理',
        code: 'sys:role:list',
        type: "目录",
        path: "/sys/role/list",
        component: "sys/Role",
        sort: '8',
        icon: 'step-forward-outlined',
        statu: '正常',
        operation: '操作',
        children: [
          {
    
    
            key: 1212,
            name: '查询',
            code: 'sys:role:list',
            type: "菜单",
            path: "",
            component: "",
            sort: '9',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1213,
            name: '新增',
            code: 'sys:role:add',
            type: "菜单",
            path: "",
            component: "",
            sort: '10',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1214,
            name: '修改',
            code: 'sys:role:edit',
            type: "菜单",
            path: "",
            component: "",
            sort: '11',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 1215,
            name: '删除',
            code: 'sys:role:delete',
            type: "菜单",
            path: "",
            component: "",
            sort: '12',
            icon: 'step-forward-outlined',
            statu: '正常',
            operation: '操作',
          },
          {
    
    
            key: 121,
            name: '重置密码',
            code: 'sys:user:repass',
            type: "按钮",
            path: "",
            component: "",
            sort: '13',
            icon: 'step-forward-outlined',
            statu: '禁用',
            operation: '操作',
          },
        ],
      },
    ],
  },
];

const rowSelection = {
    
    
  onChange: (selectedRowKeys: (string | number)[], selectedRows: DataItem[]) => {
    
    
    console.log(`selectedRowKeys: ${
      
      selectedRowKeys}`, 'selectedRows: ', selectedRows);
  },
  onSelect: (record: DataItem, selected: boolean, selectedRows: DataItem[]) => {
    
    
    console.log(record, selected, selectedRows);
  },
  onSelectAll: (selected: boolean, selectedRows: DataItem[], changeRows: DataItem[]) => {
    
    
    console.log(selected, selectedRows, changeRows);
  },
};
</script>

<style scoped>

</style>

在这里插入图片描述

(六)用户管理

用户的增删改查以及对应的权限
在这里插入图片描述

<template>
  <div>
    <a-form
        ref="formRef"
        :model="formState"
        :rules="rules"
    >
      <a-row>
        <a-col :span="12">
          <a-form-item>
            <a-input-search
                v-model:value="searchValue"
                placeholder="请输入用户名"
                enter-button="搜索"
                @search="onSearch"
            />
          </a-form-item>
        </a-col>
        <a-col :span="12">
          <a-form-item :wrapper-col="{ span:6 }">
            <a-button type="primary" @click="showDrawer">新建</a-button>
          </a-form-item>
        </a-col>
      </a-row>
    </a-form>
    <a-table :columns="columns" :data-source="data">
      <template #avatar="{ text }">
        <a-avatar :src="text" />
      </template>
      <template #name="{ text }">
        <a>{
    
    {
    
     text }}</a>
      </template>
      <template #customTitle>
      <span>
        <smile-outlined />
        Name
      </span>
      </template>
      <template #tags="{ text: tags }">
      <span>
        <a-tag
            v-for="tag in tags"
            :key="tag"
        >
          {
    
    {
    
     tag.toUpperCase() }}
        </a-tag>
      </span>
      </template>
      <template #action="{ record }">
        <span>
          <a>分配角色</a>
          <a-divider type="vertical" />
          <a>重置密码</a>
          <a-divider type="vertical" />
          <a @click="edit(record)" class="ant-dropdown-link">
            编辑
          </a>
          <a-divider type="vertical" />
           <a>删除</a>
          <a-divider type="vertical" />
        </span>
      </template>
    </a-table>
    <a-drawer
        title="添加用户"
        :width="600"
        :visible="visible"
        @close="handleClose"
    >
      <a-form
          ref="formRef"
          name="custom-validation"
          :model="formState"
          :rules="rulesFrom"
          v-bind="layout"
          @finish="handleFinish"
          @validate="handleValidate"
          @finishFailed="handleFinishFailed"
      >
        <a-form-item has-feedback label="菜单名称" name="name">
          <a-input v-model:value="formState.name" type="name" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="头像" name="avatar">
          <a-upload
              v-model:file-list="formState.avatar"
              name="avatar"
              list-type="picture-card"
              class="avatar-uploader"
              :show-upload-list="false"
              action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
              :before-upload="beforeUpload"
              @change="handleChange"
          >
            <img v-if="imageUrl" :src="imageUrl" alt="avatar" />
            <div v-else>
              <loading-outlined v-if="loading"></loading-outlined>
              <plus-outlined v-else></plus-outlined>
              <div class="ant-upload-text">Upload</div>
            </div>
          </a-upload>
        </a-form-item>
        <a-form-item has-feedback label="权限编码" name="code">
          <a-input v-model:value="formState.code" type="code" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="电话" name="phone">
          <a-input v-model:value="formState.phone" type="phone" autocomplete="off" />
        </a-form-item>
        <a-form-item has-feedback label="性别" name="sex">
          <a-select v-model:value="formState.sex" placeholder="请选择性别">
            <a-select-option value="1"></a-select-option>
            <a-select-option value="2"></a-select-option>
          </a-select>
        </a-form-item>
        <a-form-item has-feedback label="状态" name="statu">
          <a-select v-model:value="formState.statu" placeholder="请选择状态">
            <a-select-option value="1">正常</a-select-option>
            <a-select-option value="2">停止</a-select-option>
            <a-select-option value="3">注销</a-select-option>
          </a-select>
        </a-form-item>
        <a-form-item :wrapper-col="{ span: 20, offset: 4 }">
          <a-button type="primary" html-type="submit">提交</a-button>
          <a-button style="margin-left: 10px" @click="resetForm">重置</a-button>
        </a-form-item>
      </a-form>
    </a-drawer>
  </div>
</template>

<script name="user" lang="ts" setup>
import {
    
    ref,reactive } from "vue";
import {
    
     SmileOutlined, DownOutlined } from '@ant-design/icons-vue';
import {
    
     PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import {
    
     message } from 'ant-design-vue';
import {
    
    Rule} from "ant-design-vue/es/form";
import {
    
    FormInstance} from "ant-design-vue";
const formRef = ref<FormInstance>();
const visible = ref<boolean>(false);
const fileList = ref([]);
const loading = ref<boolean>(false);
const imageUrl = ref<string>('');

interface FormState {
    
    
  name: string;
  avatar: string[];
  code: string;
  email: string;
  phone: string;
  sex: string;
  statu: string;
}
const formState = reactive<FormState>({
    
    
  name: '',
  avatar:  [],
  code:  '',
  email: '',
  phone:  '',
  sex:  '',
  statu:  '',
});
const layout = {
    
    
  labelCol: {
    
     span: 4 },
  wrapperCol: {
    
     span: 20 },
};
let checkName= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入姓名');
  }
};
let checkEmail= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入邮箱');
  }
};
let checkPhone= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入电话');
  }
};
let checkSex= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入菜单名称');
  }
};
let checkStatu= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择状态');
  }
};
let checkCode= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入权限编码');
  }
};
let checkAvatar= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请选择头像');
  }
};
const rulesFrom: Record<string, Rule[]> = {
    
    
  name: [{
    
     validator: checkName, trigger: 'change' }],
  avatar: [{
    
     validator: checkAvatar, trigger: 'change' }],
  code: [{
    
     validator: checkCode, trigger: 'change' }],
  email: [{
    
     validator: checkEmail, trigger: 'change' }],
  phone: [{
    
     validator: checkPhone, trigger: 'change' }],
  sex: [{
    
     validator: checkSex, trigger: 'change' }],
  statu: [{
    
     validator: checkStatu, trigger: 'change' }],
};
const onFinish = (values: any) => {
    
    
  console.log('Success:', values);
};
let searchValue = ref("")
const columns = [
  {
    
    
    title: '头像',
    dataIndex: 'avatar',
    key: 'avatar',
    slots: {
    
    
      title: 'customTitle',
      customRender: 'avatar'
    },
  },
  {
    
    
    title: '名称',
    dataIndex: 'name',
    key: 'name',
    slots: {
    
    
      title: 'customTitle',
      customRender: 'name'
    },
  },
  {
    
    
    title: '角色',
    dataIndex: 'code',
    key: 'code',
  },
  {
    
    
    title: '邮箱',
    dataIndex: 'email',
    key: 'email',
  },
  {
    
    
    title: '电话',
    dataIndex: 'phone',
    key: 'phone',
  },
  {
    
    
    title: 'Tags',
    key: 'tags',
    dataIndex: 'tags',
    slots: {
    
    
      customRender: 'tags'
    },
  },
  {
    
    
    title: 'Action',
    key: 'action',
    slots: {
    
    
      customRender: 'action'
    },
  },
];
const data = [
  {
    
    
    key: '1',
    avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
    name: 'John Brown',
    code: 'user',
    email: '[email protected]',
    phone: '18086256816',
    tags: ['正常'],
  },
  {
    
    
    key: '2',
    avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
    name: 'Jim Green',
    code: 'doctor',
    email: '[email protected]',
    phone: '15024511186',
    tags: ['注销'],
  },
  {
    
    
    key: '3',
    avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
    name: 'Joe Black',
    code: 'admin',
    email: '[email protected]',
    phone: '15748163055',
    tags: ['正常'],
  },
];
const onSearch = () => {
    
    

}

let checkRemark= async (_rule: Rule, value: string) => {
    
    
  if (!value) {
    
    
    return Promise.reject('请输入描述');
  }
};

const rules: Record<string, Rule[]> = {
    
    
  name: [{
    
     validator: checkName, trigger: 'change' }],
  code: [{
    
     validator: checkCode, trigger: 'change' }],
  remark: [{
    
     validator: checkRemark, trigger: 'change' }],
  statu: [{
    
     validator: checkStatu, trigger: 'change' }],
};

const showDrawer = (id:number) => {
    
    
  console.log(id)
  visible.value = true;
};
const handleClose = () => {
    
    
  visible.value = false;
};
const handleFinish = (values: FormState) => {
    
    
  console.log(values, formState);
};
const handleFinishFailed = (errors: any) => {
    
    
  console.log(errors);
};
const resetForm = () => {
    
    
  // @ts-ignore
  formRef.value.resetFields();
};
const handleValidate = (...args: any[]) => {
    
    
  console.log(args);
};
interface FileItem {
    
    
  uid: string;
  name?: string;
  status?: string;
  response?: string;
  url?: string;
  type?: string;
  size: number;
  originFileObj: any;
}
interface FileInfo {
    
    
  file: FileItem;
  fileList: FileItem[];
}
function getBase64(img: Blob, callback: (base64Url: string) => void) {
    
    
  const reader = new FileReader();
  reader.addEventListener('load', () => callback(reader.result as string));
  reader.readAsDataURL(img);
}
const handleChange = (info: FileInfo) => {
    
    
  if (info.file.status === 'uploading') {
    
    
    loading.value = true;
    return;
  }
  if (info.file.status === 'done') {
    
    
    // Get this url from response in real world.
    getBase64(info.file.originFileObj, (base64Url: string) => {
    
    
      imageUrl.value = base64Url;
      loading.value = false;
    });
  }
  if (info.file.status === 'error') {
    
    
    loading.value = false;
    message.error('upload error');
  }
};

const beforeUpload = (file: FileItem) => {
    
    
  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
  if (!isJpgOrPng) {
    
    
    message.error('You can only upload JPG file!');
  }
  const isLt2M = file.size / 1024 / 1024 < 2;
  if (!isLt2M) {
    
    
    message.error('Image must smaller than 2MB!');
  }
  return isJpgOrPng && isLt2M;
};
const edit = (e:any) => {
    
    
  resetForm();
  console.log(e)
  formState.avatar = [e.avatar];
  formState.code = e.code;
  formState.email = e.email;
  formState.name = e.name;
  formState.phone = e.phone;
  formState.sex = e.sex;
  formState.statu = e.tags[0];
  visible.value = true;
}

</script>

<style scoped>

</style>

猜你喜欢

转载自blog.csdn.net/qq_44757034/article/details/128527546
今日推荐