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

news/2024/11/9 10:06:14/

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

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

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

  • 一、前言
  • 二、项目创建基本页面搭建
    • (一)创建Vue3 + TypeScript项目
      • 1、新建Vue3 项目
      • 2、用WebStorm打开项目
        • 1)打开项目以后执行 `npm install`
        • 2)安装TypeScript
        • 3)设置一下WebStorm配置
      • 3、配置项目
        • 1)安装依赖
        • 2)路由配置
        • 3)pinia配置
        • 4)vite.config.ts配置
        • 5)按需引入ant-design-vue
        • 6)安装axios
      • 4、设置页面路由
    • (二)实现登录页面
      • 1、设置登陆页面
      • 2、设置登录请求
      • 3、创建mock.ts
      • 4、显示验证码
      • 5、在store当中的store.ts设置SET_TOKEN
      • 6、实现登录请求相关内容
      • 7、完善登录
        • (1)在main.ts当中引入antd的全局样式
        • (2)完善request.ts当中响应的内容
  • 三、后台管理界面开发
    • (一)创建index页面
      • 1、新建index页面
      • 2、设置路由
      • 3、完善菜单页面内容
      • 4、Vue代码抽取
        • (1)抽取菜单
        • (2)index
      • 5、设置子路由
      • 6、编写导航栏路由
        • 1)创建需要路由跳转的页面
        • 2)设置页面路由
    • (二)用户登录信息展示
      • 1、完善用户接口
      • 2、设置mock.js
      • 3、设置个人中心的路由
      • 4、设置退出登录
    • (三)动态菜单开发
      • 1、修改一下路由规则
      • 2、设置动态菜单的数据
        • (1)自定义Icon组件
        • (2)SideMenu.vue菜单页面
        • (3)创建保存菜单的状态信息的内容
        • (4)设置mockjs
        • (5)完善SideMenu.vue菜单页面,设置请求并渲染菜单
      • 3、设置动态路由加载一次以后无需二次加载
      • 4、实现动态导航
      • 5、设置侧栏和页面进行动态绑定
      • 6、完善Tabs标签页
    • (四)菜单管理界面开发
      • 1、在Menu当中设置表格样式
      • 2、在Menu当中设置新增和编辑
    • (五)角色管理
      • 1、设置角色信息的增删改查-权限分配
    • (六)用户管理

一、前言

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

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-ignorepersist: { //数据默认存在 sessionStorage 里,并且会以 store 的 id 作为 keyenabled: 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-formref="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-imagestyle="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-itemlabel="账号"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-itemlabel="密码"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-itemlabel="验证码"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.Randomlet Result = {code: 200,msg: '操作成功',data: null
}Mock.mock('/bilibili-api/captcha','get',()=>{// @ts-ignoreResult.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 = tokenlocalStorage.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 = 404Result.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-ignoreconfig.headers['Authorization'] = localStorage.getItem("token")return config;
});
service.interceptors.response.use(response => {let res = response.dataif (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-menuv-model:selectedKeys="selectedKeysTop"theme="dark"mode="horizontal":style="{ lineHeight: '64px',marginLeft:'-15px' }"><a-sub-menu key="sub2" ><template #title><div ><menu-unfold-outlinedv-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-menuv-model:selectedKeys="selectedKeysTop"theme="dark"mode="horizontal":style="{ lineHeight: '64px',marginLeft:'-15px' }"><a-sub-menu key="sub2" ><template #title><div ><menu-unfold-outlinedv-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-ignoreResult.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-formref="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 => {//拿到menuListuserStore().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-ignorelet authoritys = [];// @ts-ignoreResult.data = {nav: nav,// @ts-ignoreauthoritys: 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:falsegetHasRoutes: (state) => state.hasRoutes,changeRouteStatus(hasRoutes:any){this.hasRoutes = hasRoutes;
}

在这里插入图片描述

router.beforeEach((to,from,next)=>{let hasRoutes = userStore().getHasRoutes;if(!hasRoutes){nav().then(res => {//拿到menuListuserStore().setMenuList(res.data.data.nav)userStore().setAuthoritys(res.data.data.authoritys)hasRoutes = trueuserStore().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-ignoreif (uStore.key === targetKey) {lastIndex = i - 1;}});// @ts-ignoreuStore = uStore.filter(pane => pane.key !== targetKey);if (uStore.length && activeKey.value === targetKey) {if (lastIndex >= 0) {// @ts-ignoreactiveKey.value = uStore[lastIndex].key;} else {// @ts-ignoreactiveKey.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().getEditableTabslet 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-formref="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-menuv-model:selectedKeys="selectedKeysTop"theme="dark"mode="horizontal":style="{ lineHeight: '64px',marginLeft:'-15px' }"><a-sub-menu key="sub2" ><template #title><div ><menu-unfold-outlinedv-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.datauserInfo = 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-formref="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-drawertitle="添加菜单":width="600":visible="visible":body-style="{ paddingBottom: '80px' }":footer-style="{ textAlign: 'right' }"@close="onClose"><a-formref="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-cascaderv-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-ignoreif (formState.checkPass !== '') {// @ts-ignoreformRef.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-ignoreformRef.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-formref="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-drawertitle="添加菜单":width="600":visible="visible":body-style="{ paddingBottom: '80px' }":footer-style="{ textAlign: 'right' }"@close="onClose"><a-formref="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-cascaderv-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-ignoreif (formState.checkPass !== '') {// @ts-ignoreformRef.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-ignoreformRef.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-formref="formRef":model="formState":rules="rules"><a-row><a-col :span="12"><a-form-item><a-input-searchv-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-tagv-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-drawertitle="添加用户":width="600":visible="visible"@close="handleClose"><a-formref="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-uploadv-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: '2800967183@qq.com',phone: '18086256816',tags: ['正常'],},{key: '2',avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',name: 'Jim Green',code: 'doctor',email: '2019967083@qq.com',phone: '15024511186',tags: ['注销'],},{key: '3',avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',name: 'Joe Black',code: 'admin',email: '2079901021@qq.com',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-ignoreformRef.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>

http://www.ppmy.cn/news/31116.html

相关文章

sHMIctrl 曲线控件使用

目录 效果 官方介绍 ​编辑 名词解释 使用方法 显示点 点在X轴位置 点在Y轴位置 量程 单位 如何设置自动标尺数据 设置量程 设置单位 效果 官方介绍 名词解释 四条曲线&#xff1a;同时最多使用4条曲线。 每条条曲线最多512点&#xff1a;X2-X1-Xn&#xff1b; 数据自定…

华为OD机试 - 不等式(C 语言解题)【独家】

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 使用说明本期题目:不等式题…

大数据开发 - Java入门3

目录2.5 数据类型2.6 变量练习1&#xff1a;采用所有的数据类型&#xff0c;都进行遍历的声明、赋值、使用练习2&#xff1a;采用变量&#xff0c;最终达到的目的&#xff0c;输出个人信息&#xff0c;姓名&#xff0c;年龄&#xff0c;身高&#xff0c;性别&#xff0c;手机号…

【Linux快速入门】文件目录操作

文章目录概念1. Linux文件系统概述2. Linux文件目录结构3. Linux文件和目录操作3.1 文件操作3.1.1 创建文件3.1.2 复制文件3.1.3 移动文件3.1.4 删除文件3.1.5 查看文件3.1.6 输出指令3.1.7 >和>>指令3.2 目录操作3.2.1 创建目录3.2.2 复制目录3.2.3 移动目录3.2.4 删…

MySQL 数据库运维与管理相关命令

最开始&#xff0c;服务器部署使用的宝塔面板&#xff0c;数据库运维管理全都是在宝塔上可视化操作&#xff0c;并没有真正的面对数据库执行过命令。出于某些原因&#xff0c;后续不准备再继续使用宝塔面板&#xff0c;所以&#xff0c;记录一下关于 MySQL 数据库管理方面相关的…

Echarts 水波图实现

开发的项目中需要实现这样一个水波图&#xff0c;例如下图在echarts官网中找了很久没找到&#xff0c;后面是在Echarts社区中找到的&#xff0c;实现了大部分的样式&#xff0c;但是还有一些数据的展示没有实现。水波图的数值展示是默认整数百分比&#xff0c;我的需求是需要保…

【专项训练】泛型递归、树的递归

递归和循环没有明显的边界! 不要进行人肉递归! 找最近重复子问题,直接写递归! 数学归纳法思维:1,2,…… 70. 爬楼梯 https://leetcode.cn/problems/climbing-stairs/ 互斥,且加在一起是全部答案! 动态规划法:用数组做递推,就是动态规划!!! class Solution

UART 串口通信

第18.1讲 UART串口通信原理讲解_哔哩哔哩_bilibili 并行通信 一个周期同时发送8bit的数据&#xff0c;占用引脚资源多 串行通信 串行通信的通信方式&#xff1a; 同步通信 同一时钟下进行数据传输 异步通信 发送设备和接收设备的时钟不同 但是需要约束波特率&#xff08;…