一、集成ant-design-vue
官网地址:Ant Design Vue — An enterprise-class UI components based on Ant Design and Vue.js
-
下载antd插件
yarn add ant-design-vue
-
在main.ts文件中引入antd
import Antd from "ant-design-vue"
import "ant-design-vue/dist/antd.css"
app.use(Antd)
二、登录模块
1、登录静态页面
<template>
<div style="background: #001529; height:100vh">
<a-card title="用户登录" :bordered="true" class="loginBox">
<a-form
:model="formState"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
label="账号"
name="account"
:rules="[{ required: true, message: '请输入账号!' }]"
>
<a-input v-model:value="formState.account" />
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: true, message: '请输入密码!' }]"
>
<a-input-password v-model:value="formState.password" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">登录</a-button>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script lang="ts">
interface FormState {
account: string;
password: string;
}
import { reactive } from "vue";
export default {
setup() {
const formState = reactive<FormState>({
account: "",
password: "",
});
const onFinish = (values: any) => {
console.log("Success:", values);
};
const onFinishFailed = (errorInfo: any) => {
console.log("Failed:", errorInfo);
};
return {
formState,
onFinish,
onFinishFailed,
};
},
};
</script>
<style>
.loginBox{
width: 380px;
position:absolute;
top:50%;
left: 50%;
transform: translate(-50%,-50%);
}
</style>
2、登录接口编写
-
安装axios
yarn add axios
-
定义UserType接口
export default interface UserType{
account: string,
password:string
}
-
编写登录接口
import axios from 'axios'
axios.defaults.baseURL="http://www.zhaijizhe.cn:3001"
import UserType from '../../type/UserType'
export default{
loginApi:(data:UserType)=>axios.post("/users/login",data)
}
-
汇总api
import users from './moudules/users'
export default{
users
}
3、登录组件中调用api方法
在Login.vue组件的onFinish函数中调用登录api方法实现用户登录
import { reactive } from "vue";
import {useRouter} from 'vue-router'
import { message } from 'ant-design-vue';
import api from "../http/api";
export default {
setup() {
const router=useRouter() //实例化路由对象
const formState = reactive<FormState>({
account: "",
password: "",
});
const onFinish = async(values: any) => {
const result=await api.users.loginApi(values)
if(result.data.code){
//将token保存到localStorage中
localStorage.setItem('token',result.data.data.token)
//将用户信息保存到状态机中
//进行路由跳转
router.replace({
path:'/'
})
}else{
message.error('用户名或密码有误!');
}
};
}
4、编写拦截器
在src/http下新建interceptor.js文件,将axios拦截器的代码编写在这里
import axios from "axios";
import { message } from 'ant-design-vue';
axios.interceptors.request.use((config:any)=>{
//携带token到请求头
config.headers.Authorization=localStorage.getItem('token')
return config;
})
axios.interceptors.response.use(res=>{
console.log('------拦截器-------');
if(res.data.code==1){
return res;
}else{
message.error( res.data.msg);
return Promise.reject(res);
}
},err=>{
switch(err.response.status){
case 500:
message.error("服务端后台出现500错误");
break;
case 401:
message.error("服务端后台出现401错误");
break;
case 404:
message.error("没有找到服务端相应资源");
break;
default:
}
return Promise.reject(err);
})
三、后台首页
1、后台首页设计
<template>
<a-layout>
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
<div class="logo">
<img src="../assets/logo.png" alt="">
企业管理系统
</div>
<a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="inline">
<a-menu-item key="1">
<user-outlined />
<span>nav 1</span>
</a-menu-item>
<a-menu-item key="2">
<video-camera-outlined />
<span>nav 2</span>
</a-menu-item>
<a-menu-item key="3">
<upload-outlined />
<span>nav 3</span>
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 2">
<menu-unfold-outlined
v-if="collapsed"
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
</a-layout-header>
<a-layout-content
:style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '580px' }"
>
Content
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script lang="ts">
import {
UserOutlined,
VideoCameraOutlined,
UploadOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
} from '@ant-design/icons-vue';
import { defineComponent, ref } from 'vue';
export default defineComponent({
components: {
UserOutlined,
VideoCameraOutlined,
UploadOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
},
setup() {
return {
selectedKeys: ref<string[]>(['1']),
collapsed: ref<boolean>(false),
};
},
});
</script>
<style>
.logo{
height: 50px;
color: white;
font-size: 18px;
line-height: 50px;
text-align: center;
}
.logo img{
width: 40px;
height: 40px;
border-radius: 50%;
}
#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: #1890ff;
}
#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>
2、二级路由配置
首先在<a-menu-item>
标签的key属性中写上二级路由的path路径保持一致
其次在<a-menu>
标签上绑定@click="handClick"方法
<a-menu
v-model:selectedKeys="selectedKeys"
theme="dark"
mode="inline"
@click="handClick">
<a-menu-item key="/userList">
<user-outlined />
<span>用户管理</span>
</a-menu-item>
<a-menu-item key="/categoryList">
<video-camera-outlined />
<span>分类管理</span>
</a-menu-item>
<a-menu-item key="/goodsList">
<upload-outlined />
<span>商品管理</span>
</a-menu-item>
</a-menu>
import {useRouter} from 'vue-router'
export default defineComponent({
setup() {
const router=useRouter()
const handClick=(item:any)=>{
router.push(item.key)
}
return {
handClick
};
},
})
设置二级路由出口
<a-layout-content
:style="{ margin: '24px 16px', padding: '24px', background: '#fff',
minHeight: '580px' }">
<!--二级路由出口 -->
<router-view></router-view>
</a-layout-content>
3、配置路由守卫
在src/router/index.ts中配置路由守卫
router.beforeEach(to=>{
const token=localStorage.getItem("token")
if(!token&&to.path!=="/login"){
return "/login"
}
})
四、动态菜单和路由挂载
1、动态菜单
-
首先通过创建pinia仓库
//导入defineStore函数
import {defineStore} from 'pinia'
import api from '@/api'
import IUserState from '@/types/IUserState'
import {RouteRecordRaw} from 'vue-router'
import {getViews} from '@/utils/routedync'
//通过该函数创建store对象
const useUserStore=defineStore('users',{
state:():IUserState=>{
return{
token:'',
permissionList:[]
}
},
getters:{
//返回首页路由对象
getHomeRoute(state:IUserState){
let routeObj:RouteRecordRaw={
path:'/',
component:()=>import('@/views/Home.vue'),
children:[]
}
let ary:Array<RouteRecordRaw>=[]
state.permissionList.forEach((item:any)=>{
if(item.children){
item.children.forEach((subItem:any)=>{
ary.push({
path:subItem.path,
component:getViews(`../views${subItem.path}.vue`)
})
})
}
})
routeObj.children=ary
return routeObj
}
},
actions:{
setToken(token:string){
this.token=token
},
async getAuthMenuAsync(){
const result=await api.users.getAuthMenus()
this.permissionList=result.data.data
console.log('权限',result.data.data);
}
},
persist:{
enabled:true,
strategies:[
{
key:'users',
storage:localStorage
}
]
}
})
export default useUserStore
-
其中要注意,这里边不能使用路由懒加载,动态路由的组件地址全部获取,可以在utils文件夹下编写routedync.ts
export function getViews(path:string){
let modules=import.meta.glob('../**/*.vue')
return modules[path]
}
-
当然,还学要在store文件夹下创建index.ts
import {createPinia} from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const pinia=createPinia()
pinia.use(piniaPluginPersist)
export default pinia
-
然后在main.ts中引入
import {createApp} from 'vue'
import App from '@/App.vue'
import Antd from "ant-design-vue"
import "ant-design-vue/dist/antd.css"
import router from './router'
import pinia from './store'
import * as icons from '@ant-design/icons-vue'
const app=createApp(App)
for (const i in icons) {
app.component(i, icons[i])
}
app.use(pinia) //设置pinina插件到vue实例上
app.use(Antd) //设置antd插件到vue实例上
app.use(router) //设置router插件到vue实例上
app.mount('#app')
-
在Home.vue中调用并渲染
<template>
<div class="container">
<div class="sider">
<!-- <ul>
<li>
<router-link to="/users">用户管理</router-link>
</li>
<li>
<router-link to="/product">商品管理</router-link>
</li>
</ul> -->
<a-menu
mode="inline"
theme="dark">
<a-sub-menu v-for="item in permissionList" :key="item._id">
<template #icon>
<component :is="item.icon"></component>
</template>
<template #title>{{item.title}}</template>
<a-menu-item :key="subItem._id" v-for="subItem in item.children" @click="go(subItem.path)">
<template #icon>
<component :is="subItem.icon"></component>
</template>
<span>{{ subItem.title}}</span>
</a-menu-item>
</a-sub-menu>
</a-menu>
<!-- {{ permissionList }} -->
</div>
<div class="content">
<!-- 二级路由出口 -->
<router-view></router-view>
</div>
</div>
</template>
<script lang='ts' setup>
import {Menu} from 'ant-design-vue'
import useUserStore from '@/store/users';
import {useRouter} from 'vue-router'
const store=useUserStore()
const nav=useRouter()
const permissionList=store.permissionList
const go=(path:string)=>{
console.log('path',path);
nav.push({
path
})
}
</script>
2、图标的动态渲染
-
下载完成后在 main.js 中添加
import { createApp } from 'vue'
import App from './App.vue'
import * as Icons from '@ant-design/icons-vue'
const app = createApp(App)
// 注册图标组件
for (const i in Icons) {
app.component(i, Icons[i])
}
app.mount('#app)
-
在vue文件中使用
<component :is="icon">
<a-sub-menu v-for="item in permissionList" :key="item._id">
<template #icon>
<component :is="item.icon"></component>
</template>
</a-sub-menu>
3、动态路由挂载
在router/index.ts中编写动态路由挂载方法
import {RouteRecordRaw,createRouter,createWebHashHistory,createWebHistory} from 'vue-router'
//定义路由规则对象集合
import api from '@/api'
import {message} from 'ant-design-vue'
// import useUserStore from '@/store/users'
const routes:Array<RouteRecordRaw>=[
{
path:'/login',
name:'login',
component:()=>import('@/views/Login.vue')
},
{
path:'/',
redirect:'/home'
},
{
path:'/home',
component:()=>import('@/views/Home.vue'),
}
]
//这里是关键代码
const dyncRoute=()=>{
const store=useUserStore()
//完成向pinia发送请求,然后获取权限
store.getAuthMenuAsync()
//调用首页路由对象
const homeRoute:RouteRecordRaw=store.getHomeRoute
//添加到路由对象上
router.addRoute(homeRoute)
console.log('路由集合',router.getRoutes());
}
//定义router对象
const router=createRouter({
routes:routes,
//history:createWebHashHistory() //hash模式,
history:createWebHistory() //history模式
})
router.beforeEach(async(to,from,next)=>{
//执行useUserStore方法
if(to.path=="/login"){
next()
}else{
//没有登录直接进入
//获取token
let token=localStorage.getItem('token')
console.log('token是否',token);
//如果没有token
if(!token){
//跳转到登录页面上去
next("/login")
}else{
try {
await api.users.getUserInfo()
dyncRoute()
//继续进行导航
next()
} catch (error) {
message.error('token已经失效,请重新登录')
next('/login')
}
}
}
})
export default router
4、页面刷新路由丢失
路由丢失的主要原因是因为执行顺序的问题,解决办法是将动态路由的添加移到main.ts中 ,删除掉router/index.ts中的动态路由添加,一定要注意动态路由的添加必须放在app.use(router)之前,app.use(pinia)之后
import {createApp} from 'vue'
import App from '@/App.vue'
import Antd from "ant-design-vue"
import "ant-design-vue/dist/antd.css"
import router from './router'
import pinia from './store'
import * as icons from '@ant-design/icons-vue'
import useUserStore from '@/store/users'
import {RouteRecordRaw} from 'vue-router'
const app=createApp(App)
for (const i in icons) {
app.component(i, icons[i])
}
const dyncRoute=()=>{
const store=useUserStore()
//完成向pinia发送请求,然后获取权限
store.getAuthMenuAsync()
//调用首页路由对象
const homeRoute:RouteRecordRaw=store.getHomeRoute
//添加到路由对象上
router.addRoute(homeRoute)
console.log('路由集合',router.getRoutes());
}
app.use(pinia) //设置pinina插件到vue实例上
app.use(Antd) //设置antd插件到vue实例上
dyncRoute()
//在app.use(router)之前进行路由添加调用,可以解决页面刷新的问题
app.use(router) //设置router插件到vue实例上
app.mount('#app')
五、选项卡
整体的思路就是封装一个选项卡组件
1、新建选项卡store
import {defineStore} from 'pinia'
import {RouteMeta} from 'vue-router'
import {unique} from '@/utils/aryutils'
interface IRouteType{
tabAry:Array<RouteMeta>
}
const useTabStore=defineStore('tabs',{
state:():IRouteType=>{
return{
tabAry:[]
}
},
getters:{
getTabAry(state){
return state.tabAry
}
},
actions:{
addTabs(item:RouteMeta){
this.tabAry.push(item)
//删除掉数组中的空对象
var filterNullAry = this.tabAry.filter(value => Object.keys(value).length !== 0);
// let index=0
// this.tabAry.forEach((value:RouteMeta,i:number)=>{
// if( Object.keys(value).length==0){
// index=i;
// }
// })
// this.tabAry[index]={title:'工作台',path:'/home/workplace'}
// //按照对象属性去重
let uniqueAray= unique(this.tabAry,"title")
this.tabAry=uniqueAray
},
removeTab(title:string){
const ary=this.tabAry.filter((item:RouteMeta)=>{
return item.title!=title
})
this.tabAry=ary
}
}
})
export default useTabStore
注意:这里封装一个按照对象进行数组中去重的方法
export const unique=(arr:any,u_key:string)=> {
let map = new Map()
arr.forEach((item:any,index:number)=>{
if (!map.has(item[u_key])){
map.set(item[u_key],item)
}
})
return [...map.values()]
}
2、在路由守卫函数中添加选项卡
router.beforeEach(async(to,from,next)=>{
//执行useTabStore方法
const store=useTabStore()
if(to.path=="/login"){
next()
}else{
//没有登录直接进入
//获取token
let token=localStorage.getItem('token')
console.log('token是否',token);
//如果没有token
if(!token){
//跳转到登录页面上去
next("/login")
}else{
try {
await api.users.getUserInfo()
store.addTabs(to.meta) //关键代码
next()
} catch (error) {
message.error('token已经失效,请重新登录')
next('/login')
}
}
}
})
3、封装选项卡组件
<template>
<div>
<div class="tab" v-for="(item,index) in tabs"
:key="index" @click="go(item.path)">{{item.title }}<span style="margin-left:10px" @click="removeTab(item.title)">×</span></div>
</div>
</template>
<script lang='ts' setup>
import { defineProps } from "vue";
import { RouteMeta,useRouter } from "vue-router";
import useTabStore from '@/store/tabs'
defineProps<{ tabs: Array<RouteMeta> }>();
const router=useRouter()
const store=useTabStore()
const go=(path:string)=>{
router.push(path)
}
const removeTab=(title:string)=>{
store.removeTab(title)
}
</script>
<style lang='scss' scoped>
.tab {
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: 500;
height: 40px;
line-height: 40px;
text-align: center;
padding: 0 15px;
}
</style>
4、在Home组件中引用
</template>
<div class="content">
<TabComponet :tabs="tabs"></TabComponet>
<!-- 二级路由出口 -->
<router-view></router-view>
</div>
</template>
<script lang='ts' setup>
import { Menu } from "ant-design-vue";
import useUserStore from "@/store/users";
import { useRouter } from "vue-router";
import TabComponet from "@/components/TabComponet.vue";
import useTabStore from "@/store/tabs";
import {storeToRefs} from 'pinia'
const store1 = useTabStore();
const tabs = storeToRefs(store1).getTabAry;
const store = useUserStore();
const nav = useRouter();
const permissionList = store.permissionList;
const go = (path: string) => {
console.log("path", path);
nav.push({
path,
});
};
</script>
六、用户管理
1、用户列表
<template>
<a-table
:dataSource="list"
:columns="columns">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'imgUrl'">
<a-avatar :src="record.imgUrl" />
</template>
<template v-if="column.key === 'role'">
{{record.role.name}}
</template>
<template v-if="column.key === 'state'">
{{record.state==1?'正常':'禁用'}}
</template>
<template v-if="column.key === 'createDate'">
{{record.createDate.substring(0,10)}}
</template>
</template>
</a-table>
</template>
<script lang="ts">
import { reactive, toRefs,onMounted} from "vue";
import api from '../http/api'
import UserType from '../type/UserType';
export default {
setup() {
const columns = [
{
title: "用户名",
dataIndex: "account",
key: "account",
},
{
title: "邮箱",
dataIndex: "email",
key: "email",
},
{
title: "创建时间",
dataIndex: "createDate",
key: "createDate",
},
{
title:'角色',
dataIndex:'role',
key:'role',
},
{
title:'头像',
dataIndex:'imgUrl',
key:'imgUrl',
},
{
title:'状态',
dataIndex:'state',
key:'state'
}
];
const data = reactive<{list:Array<UserType>}>({
list:[]
});
onMounted(async()=>{
let result=await api.users.getAccountListApi()
console.log(result.data);
data.list=result.data.data
})
return {
columns,
...toRefs(data),
};
},
};
</script>
2、删除用户
-
给columns列表增加操作项
const columns = [
{
title: '操作',
dataIndex: 'operation',
key:'operation'
},
];
-
增加template模板项
<template v-if="column.key === 'operation'">
<a-popconfirm
v-if="list.length"
title="您确定要删除吗?"
@confirm="deleteUser(record._id)"
>
<a>删除</a>
</a-popconfirm>
</template>
-
编写deleteUser方法
const deleteUser=async(id:number)=>{
let result=await api.users.deleteAccountApi(id)
if(result.data.code){
console.log(result.data.data);
findAccount()
}
}
const findAccount=async()=>{
let result=await api.users.getAccountListApi()
data.list=result.data.data
}
onMounted(()=>{
findAccount()
})
七、分类管理
1、后台api编写
import axios from 'axios'
axios.defaults.baseURL="http://www.zhaijizhe.cn:3001"
export default{
getAllCategroyApi:()=>axios.get("/categroy/findAllCategroy")
}
2、组件
<template>
<a-table
:columns="columns"
:data-source="list"
:row-selection="rowSelection"
/>
</template>
<script lang="ts">
import { reactive, toRefs, onMounted } from "vue";
import CategoryType from "../type/CategoryType";
import api from "../http/api";
export default {
setup() {
const columns = [
{
title: "名称",
dataIndex: "label",
key: "label",
},
{
title: "值",
dataIndex: "value",
key: "value",
}
];
const data = reactive<{ list: Array<CategoryType> }>({
list: [],
});
onMounted(async () => {
const result = await api.category.getAllCategroyApi();
data.list = result.data.data;
});
return {
...toRefs(data),
columns
};
},
};
</script>
<style>
</style>