Ant-design-vue

一、集成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)">&times;</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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值