GeneralAdmin-2
main.ts。
项目搭建
创建项目
//====第一步创建项目========================
1.npm init vue
2.输入项目名称
3.选择ts语法,router,pinia
4.npm i
5.npm run dev
//====第二步进入项目删除无用的代码,保证页面是空白的
保留 src/assets/base.css
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
//将这个引入到main.js中,这个放在最上面,初始化这个模块
//安装必要的依赖
npm install --save-dev @arco-design/web-vue
npm i axios
npm install -D @types/mockjs
npm install mockjs
//安装less 因为要预留arco的主题色,所以使用less作为css的预处理会更加方便
npm install less -D
npm install less-loader -D
main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import "@/assets/base.css"//引入初始化样式(放在最前面)
import App from './App.vue'
import router from './router'
import ArcoVue from '@arco-design/web-vue'; //引入arco_design
import '@arco-design/web-vue/dist/arco.css';//引入arco_design 样式
import ArcoVueIcon from '@arco-design/web-vue/es/icon'; //引入arco_design 图标
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ArcoVue); //注册arco_design
app.use(ArcoVueIcon);//注册arco_design图标
app.mount('#app')
路由构建
src\router\index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
name:"home",
path:"/",
redirect:"/admin"
},
{
name:"web",
path:"/web",
component:()=>import("@/views/web/index.vue")
},
{
name:"login",
path:"/login",
component:()=>import("@/views/login/index.vue")
},
{
name:"admin",
path:"/admin",
component:()=>import("@/views/admin/index.vue")
}
],
})
export default router
创建目录
└─views
├─admin
├─login
└─web
项目设计
admin样式初调
src\views\admin\index.vue
<script setup lang='ts'>
import { RouterView } from 'vue-router';
</script>
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<icon-sun-fill />
<icon-moon-fill />
<icon-fullscreen />
<icon-fullsreen-exit />
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px solid var(--color-neutral-2);
.g_logo {
width: 100%;
height: 90px;
border-bottom: 1px solid var(--color-neutral-2);
}
}
.g_main {
width: calc(100% - 240px);
.g_head {
width: 100%;
height: 60px;
border-bottom: 1px solid var(--color-neutral-2);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: 1px solid var(--color-neutral-2);
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
}
}
}</style>
使用arco desgin的less变量
- 在vite.config.ts中配置vite帮忙预处理,可以配置处理lessyu
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),],
// 配置less预处理
css:{
preprocessorOptions:{
less:{
additionalData:'@import "@arco-design/web-vue/es/style/theme/global.less";',
javascriptEnabled:true,
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
- 使用,首先可以使用变量直接代替原来的元素配置,--color-neutral-2是arco-desigin的变量语法
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px solid @color-border-1;
.g_logo {
width: 100%;
height: 90px;
border-bottom: 1px solid var(--color-neutral-2);
}
}
- 也可以提取出来将border-right: 1px solid @color-border-1; 作为一个完整的变量,这样就直接使用一个简介明了的变量名称就可
<style lang='less'>
@g_border:1px solid @color-border-1;
.g_admin {
display: flex;
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: @g_border;
.g_logo {
width: 100%;
height: 90px;
border-bottom: 1px solid var(--color-neutral-2);
}
}
- 但是我们将这个提取到变量中,这样就可以全局使用这个变量,方便代码在全局中使用
src\assets\var.less【封装这个变量在这个文件中】
@import "@arco-design/web-vue/es/style/theme/global.less";
@g_border:1px solid @color-border-1;
src\views\admin\index.vue【在实际的代码中使用只用写这个变量名即可】
<script setup lang='ts'>
import { RouterView } from 'vue-router';
</script>
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<icon-sun-fill />
<icon-moon-fill />
<icon-fullscreen />
<icon-fullsreen-exit />
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: @g_border;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
}
.g_main {
width: calc(100% - 240px);
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
}
}
}</style>
vite.config.ts【记得修改配置文件将预处理的文件路径进行修改】
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),],
// 配置less预处理
css:{
preprocessorOptions:{
less:{
modifyVars:{
// 'primary-6':"red" //修改arco-disgin的主题色,默认是蓝色
},
additionalData:'@import "@/assets/var.less";',
javascriptEnabled:true,
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
主题切换
src\views\admin\index.vue【已可以实现出题切换】
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import { ref } from 'vue';
const theme = ref('')
// setTheme 切换主题色函数,根据arco-disgin的特性,将arco-theme设置成dark就是默认是黑夜模式
function setTheme(val: string){
if (val === 'dark'){
document.body.setAttribute('arco-theme','dark')
}else{
document.body.removeAttribute('arco-theme')
}
theme.value = val
}
</script>
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<icon-sun-fill v-if="theme === 'dark'" @click="setTheme('')"/>
<icon-moon-fill v-if="theme === ''" @click="setTheme('dark')"/>
<icon-fullscreen />
<icon-fullsreen-exit />
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: var(--color-bg-1); //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: @g_border;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
}
.g_main {
width: calc(100% - 240px);
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
background-color: @color-fill-2; //背景色,颜色稍微和外面的背景色有少许的不一样
}
}
}</style>
主题切换代码抽离并暴露出去
src\components\common\g_theme.ts【将主题切换函数封装入ts中并暴露出去供其他调用】
import { ref } from 'vue';
export const theme = ref('')
// setTheme 切换主题色函数,根据arco-disgin的特性,将arco-theme设置成dark就是默认是黑夜模式
export function setTheme(val: string){
if (val === 'dark'){
document.body.setAttribute('arco-theme','dark')
}else{
document.body.removeAttribute('arco-theme')
}
theme.value = val
localStorage.setItem("g_theme",val) //保存到缓存中
}
// loadTheme 获取主题色函数
export function loadTheme(){
const val = localStorage.getItem("g_theme") //从缓存中获取主题
if (val){
if (val === 'dark'){
theme.value = val
setTheme(val) //切换主题
}
}
}
loadTheme() //执行函数
src\components\common\g_theme.vue【主题切换样式】
<script setup lang='ts'>
import { theme,setTheme,loadTheme } from './g_theme';
</script>
<template>
<!-- title在span标签中才会在页面中展示,所有在外层套一个span标签 -->
<span v-if="theme === 'dark'" title="白天模式"> <icon-sun-fill @click="setTheme('')"/></span>
<span v-if="theme === ''" title="黑夜模式"><icon-moon-fill @click="setTheme('dark')"/></span>
</template>
<style scoped>
</style>
src\views\admin\index.vue【使用g_theme组件】
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import { ref } from 'vue';
import G_theme from '../../components/common/g_theme.vue';
</script>
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<G_theme></G_theme>
<icon-fullscreen />
<icon-fullsreen-exit />
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: @color-fill-1; //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: @g_border;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
}
.g_main {
width: calc(100% - 240px);
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
}
}
}</style>
全屏切换
src\components\common\g_screen.vue
<script setup lang='ts'>
import { ref } from "vue"
const isFullSreen = ref(false)
//全屏
function fullScreen() {
document.documentElement?.requestFullscreen()
isFullSreen.value = true
}
//退出全屏
function exitFullScreen() {
document?.exitFullscreen()
isFullSreen.value = false
}
</script>
<template>
<!-- title在span标签中才会在页面中展示,所有在外层套一个span标签 -->
<span v-if="!isFullSreen" title="全屏"> <icon-fullscreen @click="fullScreen" /></span>
<span v-else title="退出全屏"> <icon-fullscreen-exit @click="exitFullScreen" /></span>
</template>
<style scoped lang='scss'></style>
然后调用
src\views\admin\index.vue
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<G_theme></G_theme>
<G_screen></G_screen>
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
菜单配置
【点击菜单进行路由跳转、刷新在当前路由时自动展开路由层级、菜单栏侧边按钮展开收缩实现】
src\components\admin\g_menu.vue
(记得创建响应的视图目录结构,可以参考router/index.ts)
<script setup lang='ts'>
import { Component } from 'vue';
import { IconHome, IconUser, IconSettings } from "@arco-design/web-vue/es/icon"
import G_iconComponent from '../common/g_iconComponent.vue';
import {collapsed} from "../../components/admin/g_menu"
import { ref } from 'vue';
import router from '@/router';
import { useRoute } from 'vue-router';
const route = useRoute()
interface MenuType {
title: string
name: string
icon?: string | Component
children?: MenuType[]
}
const menuList: MenuType[] = [
{ title: "首页", name: "home", icon: IconHome },
{
title: "个人中心", name: "userCenter", icon: IconUser, children: [
{ title: "用户信息", name: "userinfo" },
]
},
{
title: "用户管理", name: "userManage", icon: IconUser, children: [
{ title: "用户列表", name: "userlist" },
]
},
{
title: "系统设置", name: "systemCenter", icon: IconSettings, children: [
{ title: "系统信息", name: "systeminfo" },
]
},
]
// menuItemClick 菜单点击跳转函数
function menuItemClick(key:string) {
router.push({
name:key
})
}
const openkeys = ref<string[]>([])
// 路由初始化函数,判断路由的层级是否是三级,将中间的路由名称赋值给openkeys可以实现层级展开,只适用于三级的路层级,三级以上的路由层级需要遍历处理给openkeys,
function initRoute(){
if (route.matched.length ===3){
openkeys.value = [route.matched[1].name as string]
}
}
initRoute()
console.log(route);
</script>
<template>
<div class="g_menu" :class="{collapsed:collapsed}">
<div class="g_menu_inner scrollbar">
<a-menu
@menu-item-click="menuItemClick"
v-model:collapsed="collapsed"
v-model:open-keys="openkeys"
:default-selected-keys="[route.name]"
show-collapse-button >
<!-- 注意这里的key必须使用name字段,因为在处理route时是通过name字段来进行判断的 -->
<template v-for="menu in menuList">
<a-menu-item :key="menu.name" v-if="!menu.children">
<template #icon>
<G_iconComponent :is="menu.icon">
{{ menu.icon }}</G_iconComponent>
</template>
{{ menu.title }}
</a-menu-item>
<a-sub-menu :key="menu.name" v-else :title="menu.title">
<template #icon>
<G_iconComponent :is="menu.icon"></G_iconComponent>
</template>
<a-menu-item :key="sub.name" v-for="sub in menu.children">
{{ sub.title }}
<template #icon>
<G_iconComponent :is="sub.icon"></G_iconComponent>
</template>
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</div>
</div>
</template>
<style lang='less'>
.g_menu {
height: calc(100vh - 90px);
position: relative;
&.collapsed{
.arco-menu-collapse-button {
left: 48px !important;
}
}
&:hover {
.arco-menu-collapse-button {
opacity: 1 !important;
}
}
.g_menu_inner {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.arco-menu {
position: inherit;
.arco-menu-collapse-button {
top: 50%;
transform: translate(-50%, -50%);
left: 240px;
transition: all .3s;
opacity: 0;
}
}
}
}
</style>
src\components\admin\g_menu.ts
import { ref } from 'vue';
// 使用ts的方式暴露出去,方便后续别的地方调佣
export const collapsed = ref(false)
src\components\common\g_iconComponent.vue
<script setup lang='ts'>
import type { Component } from 'vue';
interface Props {
is?: Component | string
}
const props = defineProps<Props>()
</script>
<!-- 处理图标显示,如果是 Component类型,直接使用component渲染,如果是字符串,使用i标签渲染-->
<template>
<component v-if="typeof props.is === 'object'" :is="props.is"></component>
<i :class="props.is" v-else></i>
</template>
src\router\index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
name: "home",
path: "/",
redirect: "/admin"
},
{
name: "web",
path: "/web",
component: () => import("@/views/web/index.vue")
},
{
name: "login",
path: "/login",
component: () => import("@/views/login/index.vue")
},
{
name: "admin",
path: "/admin",
component: () => import("@/views/admin/index.vue"),
meta:{title:"首页"},
children: [
{ name: "home", path: "", component: () => import("@/views/admin/home/index.vue") },
{
name: "userCenter", path: "user_Center", children: [{name: "userinfo",path: "user_info",component: () => import("@/views/admin/user_center/index.vue"),meta:{title:"用户信息"} }],
meta:{title:"用户中心"}
},
{
name: "userManage", path: "user_Manage", children: [
{ name: "userlist", path: "user_list", component: () => import("@/views/admin/user_manage/index.vue"),meta:{title:"用户列表"} }],
meta:{title:"用户管理"}
},
{
name: "systemCenter", path: "system_Center", children: [
{ name: "systeminfo", path: "system_info", component: () => import("@/views/admin/system_manage/index.vue"),meta:{title:"系统管理"} }],
meta:{title:"系统信息"}
},
]
}
],
})
export default router
src\views\admin\index.vue
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import G_theme from '../../components/common/g_theme.vue';
import G_screen from '../../components/common/g_screen.vue';
import G_menu from '../../components/admin/g_menu.vue'
import {collapsed} from "../../components/admin/g_menu"
</script>
<template>
<div class="g_admin" >
<!-- 给gasid 添加动态的collapsed类,在触发收缩时添加,方便给aside设置宽度 -->
<div class="g_aside" :class="{collapsed:collapsed}">
<div class="g_logo"></div>
<G_menu></G_menu>
</div>
<div class="g_main" :class="{collapsed:collapsed}">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<G_theme></G_theme>
<G_screen></G_screen>
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: var(--color-bg-1); //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
// 不能使用overflow。否则,收缩展开按钮会被覆盖
width: 240px;
height: 100vh;
border-right: @g_border;
transition: width .3s;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
//g_main下添加了collapsed类,如果存在就将这边框设置为48px即为收起状态
&.collapsed{
width: 48px;
// &~只有下兄弟标签适用,将g_main的宽度进行调整
&~.g_main{
width: calc(100% - 48px);
}
}
}
.g_main {
width: calc(100% - 240px);
transition: width .3s;
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
background-color: @color-fill-2; //背景色,颜色稍微和外面的背景色有少许的不一样
}
}
}</style>
面包屑配置
在admin/index.vue中将g_breadcrumbs换成该组件
src\components\admin\g_breadcrumbs.vue
<script setup lang='ts'>
import {type RouteMeta,useRoute} from "vue-router"
const route =useRoute()
export interface MetaType extends RouteMeta{
title:string
}
</script>
<!-- 利用router的meta的title显示中文作为面包屑,但是的严格按照路由的配置来,这里也可以添加跳转点击 -->
<template>
<a-breadcrumb>
<template v-for="r in route.matched">
<a-breadcrumb-item v-if="r.name !=='home'">{{ (r.meta as MetaType)?.title }}</a-breadcrumb-item>
</template>
</a-breadcrumb>
</template>
<style scoped lang='scss'>
</style>
iconfont图标引入
在iconfont-阿里巴巴矢量图标库 中选入好图标,添加到项目中,下载下来保存为iconfont.css到assets下,然后在main.ts中import "@/assets/iconfont.css" 即可使用
@font-face {
font-family: "iconfont"; /* Project id 5105306 */
src: url('//at.alicdn.com/t/c/font_5105306_mzdjy1g36k.woff2?t=1767773658097') format('woff2'),
url('//at.alicdn.com/t/c/font_5105306_mzdjy1g36k.woff?t=1767773658097') format('woff'),
url('//at.alicdn.com/t/c/font_5105306_mzdjy1g36k.ttf?t=1767773658097') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-xitongxinxi:before {
content: "\e609";
}
.icon-xitongguanli:before {
content: "\e652";
}
.icon-yonghuliebiao:before {
content: "\e657";
}
.icon-yonghuxiangqing:before {
content: "\e6c0";
}
.icon-account-pin-circle-line:before {
content: "\e79e";
}
.icon-user-manage:before {
content: "\e607";
}
用户下拉部分
src\components\common\g_user_dropdown.vue
<script setup lang='ts'>
import router from '@/router';
function handleSelect(val:string){
if (val === 'logout'){
// 注销登录
return
}
// 其他选项跳转到相应的页面中去
router.push({
name:val
})
}
interface OptionType {
name:string
title:string
}
// name必须与路由对上才能正确跳转
const options :OptionType[] = [
{title:"用户中心",name:"userinfo"},
{title:"用户管理",name:"userlist"},
{title:"系统信息",name:"systeminfo"},
{title:"用户注销",name:"logout"},
]
</script>
<template>
<a-dropdown @select="handleSelect" :popup-max-height="false">
<div class="g_user_dropdown_com">
<a-avatar :size="35" image-url="data:image/jpeg;base64,HE+CeI9ZpFpZsTIzT7GiSKpi6Qj5xJJotxeVhTaG0xibRF2fbD7Q+3lZQ8QPtj7D7Z//2Q=="></a-avatar>
<span class="user_name">王五</span>
<icon-down></icon-down>
</div>
<template #content>
<a-doption v-for="item in options" :value="item.name">{{ item.title }}</a-doption>
</template>
</a-dropdown>
</template>
<style lang='less'>
.g_user_dropdown_com{
cursor: pointer;
.user_name{
margin: 0 5px;
}
svg{
margin-right: 0 !important;
}
}
</style>
src\views\admin\index.vue【引入组件并调节下拉部分的样式】
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import G_theme from '../../components/common/g_theme.vue';
import G_screen from '../../components/common/g_screen.vue';
import G_menu from '../../components/admin/g_menu.vue'
import G_breadcrumbs from "../../components/admin/g_breadcrumbs.vue"
import G_user_deopdown from "../../components/common/g_user_dropdown.vue"
import {collapsed} from "../../components/admin/g_menu"
import router from '@/router';
function goHome(){
router.push({
name:"web"
})
}
</script>
<template>
<div class="g_admin" >
<!-- 给gasid 添加动态的collapsed类,在触发收缩时添加,方便给aside设置宽度 -->
<div class="g_aside" :class="{collapsed:collapsed}">
<div class="g_logo"></div>
<G_menu></G_menu>
</div>
<div class="g_main" :class="{collapsed:collapsed}">
<div class="g_head">
<G_breadcrumbs></G_breadcrumbs>
<div class="g_actions">
<span title="去首页" @click="goHome"> <icon-home></icon-home></span>
<G_theme></G_theme>
<G_screen></G_screen>
<G_user_deopdown></G_user_deopdown>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: var(--color-bg-1); //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
// 不能使用overflow。否则,收缩展开按钮会被覆盖
width: 240px;
height: 100vh;
border-right: @g_border;
transition: width .3s;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
//g_main下添加了collapsed类,如果存在就将这边框设置为48px即为收起状态
&.collapsed{
width: 48px;
// &~只有下兄弟标签适用,将g_main的宽度进行调整
&~.g_main{
width: calc(100% - 48px);
}
}
}
.g_main {
width: calc(100% - 240px);
transition: width .3s;
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
.g_actions{
display: flex;
align-items: center;
}
svg{
font-size: 18px;
cursor: pointer;
margin-right: 10px;
}
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
background-color: @color-fill-2; //背景色,颜色稍微和外面的背景色有少许的不一样
}
}
}</style>
src\components\admin\g_menu.vue【调整菜单展开的逻辑,不使用默认的,而是监听路由变化】
<script setup lang='ts'>
import { Component } from 'vue';
import { IconHome, IconUser, IconSettings } from "@arco-design/web-vue/es/icon"
import G_iconComponent from '../common/g_iconComponent.vue';
import {collapsed} from "../../components/admin/g_menu"
import { ref,watch } from 'vue';
import router from '@/router';
import { useRoute } from 'vue-router';
const route = useRoute()
interface MenuType {
title: string
name: string
icon?: string | Component
children?: MenuType[]
}
const menuList: MenuType[] = [
{ title: "首页", name: "home", icon: IconHome },
{
title: "个人中心", name: "userCenter", icon: "iconfont icon-user-manage", children: [
{ title: "用户信息", name: "userinfo",icon:"iconfot icon-account-pin-circle-line" },
]
},
{
title: "用户管理", name: "userManage", icon: "iconfont icon-user-manage", children: [
{ title: "用户列表", name: "userlist", icon: "iconfont icon-yonghuliebiao" },
]
},
{
title: "系统设置", name: "systemCenter", icon: "iconfont icon-xitongguanli", children: [
{ title: "系统信息", name: "systeminfo", icon: "iconfont icon-xitongxinxi" },
]
},
]
// menuItemClick 菜单点击跳转函数
function menuItemClick(key:string) {
router.push({
name:key
})
}
const openkeys = ref<string[]>([])
const selectedKeys = ref<string[]>([])
// 路由初始化函数,判断路由的层级是否是三级,将中间的路由名称赋值给openkeys可以实现层级展开,只适用于三级的路层级,三级以上的路由层级需要遍历处理给openkeys,
function initRoute(){
if (route.matched.length ===3){
openkeys.value = [route.matched[1].name as string]
}
// 将路由的name添加到elected-keys中,默认展开路由
selectedKeys.value = [route.name as string]
}
//监听路由变化,如果路由变化就执行一遍initRoute
watch(()=>route.name,()=>{
initRoute()
},{immediate:true})
</script>
<template>
<div class="g_menu" :class="{collapsed:collapsed}">
<div class="g_menu_inner scrollbar">
<a-menu
@menu-item-click="menuItemClick"
v-model:collapsed="collapsed"
v-model:open-keys="openkeys"
v-model:selected-keys ="selectedKeys"
show-collapse-button >
<!-- 注意这里的key必须使用name字段,因为在处理route时是通过name字段来进行判断的 -->
<template v-for="menu in menuList">
<a-menu-item :key="menu.name" v-if="!menu.children">
<template #icon>
<G_iconComponent :is="menu.icon">
{{ menu.icon }}</G_iconComponent>
</template>
{{ menu.title }}
</a-menu-item>
<a-sub-menu :key="menu.name" v-else :title="menu.title">
<template #icon>
<G_iconComponent :is="menu.icon"></G_iconComponent>
</template>
<a-menu-item :key="sub.name" v-for="sub in menu.children">
{{ sub.title }}
<template #icon>
<G_iconComponent :is="sub.icon"></G_iconComponent>
</template>
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</div>
</div>
</template>
<style lang='less'>
.g_menu {
height: calc(100vh - 90px);
position: relative;
&.collapsed{
.arco-menu-collapse-button {
left: 48px !important;
}
}
&:hover {
.arco-menu-collapse-button {
opacity: 1 !important;
}
}
.g_menu_inner {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.arco-menu {
position: inherit;
.arco-menu-collapse-button {
top: 50%;
transform: translate(-50%, -50%);
left: 240px;
transition: all .3s;
opacity: 0;
}
}
}
}
</style>
tabs组件
env.d.ts
/// <reference types="vite/client" />
// 给 RouteMeta 声明一个title类型,这样在声明对象时不会报警告,也不会飘红
import "vue-router"
declare module "ue-router"{
interface RouteMeta{
title:string
}
}
安装 swiper npm i swiper
src\components\admin\g_tabs.vue
<script setup lang='ts'>
import { ref } from 'vue';
import { IconClose } from '@arco-design/web-vue/es/icon';
import router from '@/router';
import { useRoute } from 'vue-router';
import { watch } from 'vue';
import { Swiper, SwiperSlide } from 'swiper/vue';
import { onMounted } from 'vue';
const route = useRoute()
interface TabType {
name: string
title: string
}
const tabs = ref<TabType[]>([
{ title: "首页", name: "home" },
])
function check(itme: TabType) {
router.push({
name: itme.name
})
saveTabs()
}
function removeItem(itme: TabType) {
if (itme.name === 'home') {
return
}
const index = tabs.value.findIndex((value) => itme.name === value.name)
if (index != -1) {
//判断我删除的这个元素,是不是我当前所在的
if (itme.name === route.name) {
router.push({
name: tabs.value[index - 1].name
})
}
tabs.value.splice(index, 1)
}
saveTabs()
}
function removeAllItem() {
tabs.value = [
{ title: "首页", name: "home" },
]
router.push({ name: "home" })
saveTabs()
}
// 持久化tabs
function saveTabs() {
localStorage.setItem("g_tabs", JSON.stringify(tabs.value))
}
// 初始化tabs
function loadTabs() {
const g_tabs = localStorage.getItem("g_tabs")
if (g_tabs) {
try {
tabs.value = JSON.parse(g_tabs)
} catch (e) {
console.log(e);
}
}
}
loadTabs()
watch(() => route.name, () => {
//判断当前路由的名称,在不在 tabs 里面,如果不在就加入进去
const index = tabs.value.findIndex((value) => route.name === value.name)
if (index === -1) {
tabs.value.push({
name: route.name as string,
title: route.meta.title as string,
})
}
}, { immediate: true })
const slidesCount = ref(100)
onMounted(() => {
// 先算总宽度
// 算实际宽度(scrollWidth)有没有超出总宽度
// 如果超出了总宽度,就遍历所有的 swiper-slide
// 从前往后加,如果超过了总宽度,那个时候的个数,就是实际显示的个数
// 显示总宽度
const swiperDom = document.querySelector(".g_tabs_swiper") as HTMLDivElement
const swiperWidth = swiperDom.clientWidth
// 显示实际的总宽度
const wrapperDom = document.querySelector(".g_tabs_swiper .swiper-wrapper") as HTMLDivElement
const wrapperWidth = wrapperDom.scrollWidth
if (swiperWidth > wrapperWidth) {
return
}
// 如果实际的总宽度大雨了显示的总宽度
// 遍历 swiper-slide然后从后往前加
const slideList = document.querySelectorAll(".g_tabs_swiper .swiper-slide")
let allWith = 0
let index = 1
for (const sliderListElement of slideList) {
// 加上margin的宽度 20
allWith += (sliderListElement.clientWidth + 20)
index++
if (allWith >= swiperWidth) {
break
}
}
slidesCount.value = index
// 用户刚进来这个页面时,选中高亮元素
const activateSlide = document.querySelector(".g_tabs_swiper .swiper-slide.activate") as HTMLDivElement
if (activateSlide.offsetLeft > swiperWidth){
const offsetLeft = swiperWidth - activateSlide.offsetLeft
setTimeout(()=>{
wrapperDom.style.transform = `translate3d(${offsetLeft}px,0px,0px)`
})
}
})
</script>
<template>
<div class="g_tabs">
<swiper :slides-per-view="slidesCount" class="g_tabs_swiper">
<swiper-slide v-for="item in tabs" :class="{ activate: route.name === item.name }">
<div class="item" @click="check(item)" @mousedown.middle.stop="removeItem(item)"
:class="{ activate: route.name === item.name }">{{ item.title }}
<span class="close" title="删除" @click.stop="removeItem(item)" v-if="item.name != 'home'">
<IconClose></IconClose>
</span>
</div>
</swiper-slide>
</swiper>
<div class="item" @click="removeAllItem">删除全部</div>
</div>
</template>
<style lang='less'>
.g_tabs {
display: flex;
align-items: center;
padding: 0 10px;
justify-content: space-between;
.swiper {
width: calc(100% - 100px);
display: flex;
overflow-y: hidden;
overflow-x: hidden;
.swiper-wrapper {
display: flex;
align-items: center;
}
.swiper-slide {
width: fit-content !important;
flex-shrink: 0;
}
}
.item {
padding: 1px 8px;
background-color: var(--color-bg-1);
border: @g_border;
margin-right: 10px;
cursor: pointer;
border-radius: 5px;
flex-shrink: 0;
&:hover {
background-color: var(--color-fill-1);
}
&.activate {
background-color: @primary-6;
color: white;
}
}
}</style>
然后在admin/idnex.vue中引入该组件
logo组件
src\components\admin\g_logo.vue
<script setup lang='ts'>
import { collapsed } from './g_menu';
</script>
<template>
<div class="g_logo" :class="{collapsed:collapsed}">
<img src="../../assets/logo.svg" alt="logo" class="logo">
<div class="slogan">
<div>通用后台</div>
<div>Genernaladmin</div>
</div>
</div>
</template>
<style lang='less'>
.g_logo{
display: flex;
justify-content: center;
align-items: center;
&.collapsed{
.slogan{
transform: scale(0);
transform-origin: left;
opacity: 0;
}
.logo{
transform: translateX(50px);
width: 40px;
height: 40px;
}
}
.logo{
width: 50px;
height: 50px;
}
.slogan{
margin-left: 10px;
div:nth-child(1){
font-size: 22px;
font-weight: 600;
}
div:nth-child(2){
font-size: 12px;
margin-top: 1px;
}
}
}
</style>
路由切换动画+container样式优化
src\views\admin\index.vue
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import G_theme from '../../components/common/g_theme.vue';
import G_screen from '../../components/common/g_screen.vue';
import G_menu from '../../components/admin/g_menu.vue'
import G_breadcrumbs from "../../components/admin/g_breadcrumbs.vue"
import G_user_deopdown from "../../components/common/g_user_dropdown.vue"
import {collapsed} from "../../components/admin/g_menu"
import router from '@/router';
import G_tabs from '@/components/admin/g_tabs.vue';
import G_logo from '@/components/admin/g_logo.vue';
function goHome(){
router.push({
name:"web"
})
}
</script>
<template>
<div class="g_admin" >
<!-- 给gasid 添加动态的collapsed类,在触发收缩时添加,方便给aside设置宽度 -->
<div class="g_aside" :class="{collapsed:collapsed}">
<G_logo></G_logo>
<G_menu></G_menu>
</div>
<div class="g_main" :class="{collapsed:collapsed}">
<div class="g_head">
<G_breadcrumbs></G_breadcrumbs>
<div class="g_actions">
<span title="去首页" @click="goHome"> <icon-home></icon-home></span>
<G_theme></G_theme>
<G_screen></G_screen>
<G_user_deopdown></G_user_deopdown>
</div>
</div>
<G_tabs></G_tabs>
<!-- 路由切换动画配置 -->
<div class="g_container scrollbar">
<router-view v-slot="{Component}" class="g_base_view">
<transition name="fade" mode="out-in">
<component :is="Component"></component>
</transition>
</router-view>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: var(--color-bg-1); //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
// 不能使用overflow。否则,收缩展开按钮会被覆盖
width: 240px;
height: 100vh;
border-right: @g_border;
transition: width .3s;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
//g_main下添加了collapsed类,如果存在就将这边框设置为48px即为收起状态
&.collapsed{
width: 48px;
// &~只有下兄弟标签适用,将g_main的宽度进行调整
&~.g_main{
width: calc(100% - 48px);
}
}
}
.g_main {
width: calc(100% - 240px);
transition: width .3s;
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
.g_actions{
display: flex;
align-items: center;
}
svg{
font-size: 18px;
cursor: pointer;
margin-right: 10px;
}
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
background-color: @color-fill-2; //背景色,颜色稍微和外面的背景色有少许的不一样
padding: 20px 0px 20px 20px;
.g_base_view{
background-color: var(--color-bg-1);
border-radius: 5px;
}
}
}
}
// 组件刚开始离开
.fade-leave-active {
}
// 组件离开结束
.fade-leave-to {
transform: translateX(30px);
opacity: 0;
}
// 组件刚开始进入
.fade-enter-active {
transform: translateX(-30px);
opacity: 0;
}
// 组件进入完成
.fade-enter-to {
transform: translateX(0);
opacity: 1;
}
// 正在进入和离开
.fade-leave-active, .fade-enter-active {
transition: all .3s ease-out;
}
</style>
路由进度条使用
1.安装
npm install --save nprogress
npm i -D @types/nprogress
2.在路由组件中使用
import NProgress from "nprogress";
router.beforeEach((to, from, next) => {
NProgress.start();//开启进度条
next()
})
//当路由进入后:关闭进度条
router.afterEach(() => {
// 在即将进入新的页面组件前,关闭掉进度条
NProgress.done()//完成进度条
})
在main.js中引入
import "nprogress/nprogress.css";
环境变量
1.首先修改package.json中的dev启动配置,可以通过不同的配置读取不同的.env文件中的内容
"scripts": {
"dev": "vite", //会去读取.env文件中带有前缀VITE的变量
"dev1":"vite --mode dev1", //会去读取.env.dev1文件中带有前缀VITE的变量
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
2.配置.env类似中的文件如.env.dev1
VITE_SERVERNAME='测试环境1'
VITE_SERVER_URL=http://127.0.0.1:8000
3.使用函数方式编辑配置文件vite.config.ts
import { EnvMeta } from './env.d'; //添加loadEnv的类型
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import { loadEnv } from 'vite' //加载env函数
import { log } from 'node:console';
const envDir = './' //.env文件的地址
// 使用函数方式配置
export default defineConfig((config)=>{
const env:EnvMeta =loadEnv(config.mode,envDir) as EnvMeta
log(env.VITE_SERVER_URL) //声明EnvMeta类型后可以直接点出来VITE_SERVER_URL。但是不声明的话也可以,只不过不会自动弹出来。可以在env.d.ts中声明
return {
plugins: [
vue(),
vueDevTools(),],
// 配置less预处理
css:{
preprocessorOptions:{
less:{
modifyVars:{
// 'primary-6':"red" //修改arco-disgin的主题色,默认是蓝色
},
additionalData:'@import "@/assets/var.less";',
javascriptEnabled:true,
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
// 前端地址端口以及ip配置
server:{
host:"0.0.0.0",
port:8080
},
//配置env地址
envDir:envDir
}
})
env.d.ts
/// <reference types="vite/client" />
// 给 RouteMeta 声明一个title类型,这样在声明对象时不会报警告,也不会飘红
import "vue-router"
declare module "ue-router"{
interface RouteMeta{
title:string
}
}
// 声明类型,继承自Record
export interface EnvMeta extends Record<string,string>{
VITE_SERVER_URL:string
VITE_SERVER_NAME:string
}
跨域配置
// 前端地址端口以及ip配置
server:{
host:"0.0.0.0",
port:8080,
// 跨域配置,如果后端的路由是以/api开始的,识别到后转成同源的路径进行请求
proxy:{
"/api":{
target:env.VITE_SERVER_URL,
rewrite:(path:string)=>path.replace("/api","") //这个是重写路径,如果后端的接口不满足以api开头的,那么就进行替换为空,也能完成解决跨域
}
}
},
axios封装
src\api\index.ts
import axios from "axios";
import {Message} from "@arco-design/web-vue";
// import {userStorei} from "@/stores/user_store";
import type {Ref} from "vue";
// 基础的响应类型,一般和后端项目的响应类型一致
export interface baseResponse<T> {
code: number
msg: string
data: T
}
// 列表类型的响应类型
export interface listResponse<T> {
list: T[]
count: number
}
// URL的Query Parameters参数
export interface paramsType {
key?: string
limit?: number
page?: number
sort?: string
[key: string]: any
}
export const useAxios = axios.create({
timeout: 6000,
baseURL: "", // 在使用前端代理的情况下,这里必须留空,不然会跨域
})
// axios请求拦截
useAxios.interceptors.request.use((config) => {
// const userStore = userStorei() //先去这个全局的store中拿到这个用户的token
// config.headers.set("token", userStore.userInfo.token) //将token配置到请求头中
return config
})
// axios响应拦截
useAxios.interceptors.response.use((res) => {
// 响应拦截时先做一个判断,如果状态是200才是响应成功,将数据返回出去
if (res.status === 200) {
return res.data
}
return res
}, (res) => {
// 如果有错误,将错误展示出来
Message.error(res.message)
})
// 默认删除的接口,接受一个url,一个id_list列表,返回基础的响应数据(因为后端的删除接口的请求参数都是是id_list,所以可以放在前端作为一个默认的接口方便调用)
export function defaultDeleteApi(url: string, id_list: number[]): Promise<baseResponse<string>> {
return useAxios.delete(url, {data: {id_list}})
}
// 默认post的接口,接受一个url,一个data,返回基础的响应数据
export function defaultPostApi(url: string, data: any): Promise<baseResponse<string>> {
return useAxios.post(url, data)
}
export function defaultPutApi(url: string, data: any): Promise<baseResponse<string>> {
return useAxios.put(url, data)
}
export interface optionsType {
label: string
value: number | string
}
export type optionsFunc = (params?: paramsType) => Promise<baseResponse<optionsType[]>>
export function getOptions(ref: Ref<optionsType[]>, func: optionsFunc, params?: paramsType){
func(params).then((res)=>{
ref.value = res.data
})
}
登录页面实现
1、src\api\user_api.ts【用户登录相关接口】
import { type baseResponse,useAxios} from "@/api/index";
export interface pwdLoginRequest {
val :string
password:string
}
export function pwdLoginApi(data:pwdLoginRequest):Promise<baseResponse<string>>{
return useAxios.post("api/user/login",data)
}
export interface userInfoType {
"user_id": number
"code_age": number
"avatar": string
"nickname": string
"look_count": number
"article_count": number
"fans_count": number
"place": string
"open_collect": boolean,
"open_follow": boolean,
"open_fans": boolean,
"home_style_id": number
"relationship": 0| 1 | 2 | 3 | 4
}
export function userInfoApi(userID: number): Promise<baseResponse<userInfoType>> {
return useAxios.get("/api/user/base", {params: {id: userID}})
}
src\stores\user_store.ts【pinia存储】
import {defineStore} from 'pinia'
import {userInfoApi} from "@/api/user_api";
import {Message} from "@arco-design/web-vue";
import {parseToken} from "@/utils/parse_token";
import router from "@/router";
interface userInfoType {
userID: number
nickName: string
userName: string
avatar: string
role: number
token: string
"lookCount": number
"articleCount": number
"fansCount": number
"followCount": number
"place": string
}
interface userStoreType {
userInfo: userInfoType
}
export const userStorei = defineStore('userStore', {
state: (): userStoreType => {
return {
userInfo: {
userID: 0,
nickName: "",
userName: "",
avatar: "",
role: 0,
token: "",
lookCount: 0,
articleCount: 0,
fansCount: 0,
followCount: 0,
place: ""
}
}
},
actions: {
saveUserInfo(token: string) {
// 传一个token过来,然后重新去调用户信息接口
this.userInfo.token = token
const payLoad = parseToken(token)
this.userInfo.userID = payLoad.user_id
this.userInfo.role = payLoad.role_id
userInfoApi(payLoad.user_id).then(res => {
if (res.code) {
Message.error(res.msg)
return
}
this.userInfo = {
userID: res.data.user_id,
nickName: res.data.nickname,
userName: res.data.nickname,
avatar: res.data.avatar,
role: payLoad.role_id,
token: token,
lookCount: res.data.look_count,
articleCount: res.data.article_count,
fansCount: res.data.fans_count,
followCount: res.data.fans_count,
place: res.data.place,
}
// 持久化
localStorage.setItem("g_userInfo", JSON.stringify(this.userInfo))
})
},
loadUserInfo() {
const val = localStorage.getItem("g_userInfo")
if (!val) {
return
}
try {
this.userInfo = JSON.parse(val)
} catch (e) {
console.log(e)
console.log(val)
return;
}
// 判断token有没有过期
const payLoad = parseToken(this.userInfo.token) // exp的时间是秒
const nowTime = new Date().getTime() // 单位是毫秒
if (payLoad.exp * 1000 - nowTime <= 0) {
Message.warning("token已过期")
localStorage.removeItem("g_userInfo")
router.push({name: "admin"})
return;
}
},
},
getters: {
// 判断用户是否登录
isLogin(): boolean {
return this.userInfo.userID !== 0
},
// 判断用户是否是管理员
isAdmin(): boolean {
return this.userInfo.role == 1
},
}
})
src\utils\parse_token.ts【解析token工具函数】
export interface jwtPayload {
role_id: number
user_id: number
username: string
"exp": number
"iss": string
}
export function parseToken(token: string): jwtPayload{
const payLoadString = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")
return JSON.parse(decodeURIComponent(escape(window.atob(payLoadString))))
}
src\api\index.ts【axions请求拦截补充token设置】
// axios请求拦截
useAxios.interceptors.request.use((config) => {
const userStore = userStorei() //先去这个全局的store中拿到这个用户的token
config.headers.set("Authorization", userStore.userInfo.token) //将token配置到请求头中
return config
})
src\App.vue【app中添加获取用户信息】
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import {userStorei} from "@/stores/user_store";
const store = userStorei()
store.loadUserInfo()
</script>
<template>
<RouterView />
</template>
利用用户信息展示
src\components\common\g_user_dropdown.vue【添加用户的根据角色判断展示不同的下拉选项】
// name必须与路由对上才能正确跳转
const options = ref<OptionType[]>([
{title:"用户中心",name:"userinfo"},
{title:"用户注销",name:"logout"},
])
if (store.isAdmin){
options.value = [
{title:"用户中心",name:"userinfo"},
{title:"用户管理",name:"userlist"},
{title:"系统信息",name:"systeminfo"},
{title:"用户注销",name:"logout"},
]
}
src\components\admin\g_menu_item.vue【对g_menu.vue进行在此封装】
<script setup lang="ts">
import type {Component} from "vue";
import G_iconComponent from "@/components/common/g_iconComponent.vue";
import {userStorei} from "@/stores/user_store";
import G_menu_item from "@/components/admin/g_menu_item.vue";
const store = userStorei()
interface MenuType {
title: string
name: string
icon?: string | Component
children?: MenuType[]
role?: number
}
interface Props {
list: MenuType[]
}
const props = defineProps<Props>()
</script>
<!-- 这个组件时循环判断menu组件是否展示的封装, v-if="menu.role === undefined || menu.role === store.userInfo.role" 的含义是
如果组件中的role的字段没有定义,那么就是undifined,会显示,或者说这里定义的类型是和该用户的类型相同也会显示,否则都不显示
-->
<template>
<!-- 注意这里的key必须使用name字段,因为在处理route时是通过name字段来进行判断的 -->
<template v-for="menu in props.list">
<template v-if="!menu.children">
<a-menu-item :key="menu.name" v-if="menu.role === undefined || menu.role === store.userInfo.role">
<template #icon>
<G_iconComponent :is="menu.icon"></G_iconComponent>
</template>
{{ menu.title }}
</a-menu-item>
</template>
<template v-else>
<a-sub-menu :key="menu.name" v-if="menu.role === undefined || menu.role === store.userInfo.role"
:title="menu.title">
<template #icon>
<G_iconComponent :is="menu.icon"></G_iconComponent>
</template>
<G_menu_item :list="menu.children as MenuType[]"></G_menu_item>
</a-sub-menu>
</template>
</template>
</template>
<style scoped>
</style>
src\components\admin\g_menu.vue【调用组件】
<template>
<div class="g_menu" :class="{collapsed:collapsed}">
<div class="g_menu_inner scrollbar">
<a-menu
@menu-item-click="menuItemClick"
v-model:collapsed="collapsed"
v-model:open-keys="openkeys"
v-model:selected-keys ="selectedKeys"
show-collapse-button >
<G_menu_item :list="menuList"></G_menu_item>
</a-menu>
</div>
</div>
</template>
注销功能
src\api\user_api.ts【添加用户注销的api接口】
export function userLogoutApi(): Promise<baseResponse<string>> {
return useAxios.delete("/api/user/logout")
}
src\stores\user_store.ts【添加注销等功actios】
import {userInfoApi,userLogoutApi} from "@/api/user_api";
....
....
actions: {
saveUserInfo(token: string) {
// 传一个token过来,然后重新去调用户信息接口
this.userInfo.token = token
const payLoad = parseToken(token)
this.userInfo.userID = payLoad.user_id
this.userInfo.role = this.userInfo.userID
userInfoApi(payLoad.user_id).then(res => {
if (res.code) {
Message.error(res.msg)
return
}
this.userInfo = {
userID: res.data.user_id,
nickName: res.data.nickname,
userName: res.data.nickname,
avatar: res.data.avatar,
role: payLoad.role_id,
token: token,
lookCount: res.data.look_count,
articleCount: res.data.article_count,
fansCount: res.data.fans_count,
followCount: res.data.fans_count,
place: res.data.place,
}
// 持久化
localStorage.setItem("g_userInfo", JSON.stringify(this.userInfo))
})
},
// 加载用户信息
loadUserInfo() {
const val = localStorage.getItem("g_userInfo")
if (!val) {
return
}
try {
this.userInfo = JSON.parse(val)
} catch (e) {
console.log(e)
console.log(val)
return;
}
// 判断token有没有过期
const payLoad = parseToken(this.userInfo.token) // exp的时间是秒
const nowTime = new Date().getTime() // 单位是毫秒
if (payLoad.exp * 1000 - nowTime <= 0) {
Message.warning("token已过期")
localStorage.removeItem("g_userInfo")
router.push({name: "admin"})
return;
}
},
// 用户登出-》调用用户注销接口->清空 localStorage->清空store里面的值
async userLogout() {
const res = await userLogoutApi()
localStorage.removeItem("g_userInfo")
this.userInfo = {
userID: 0,
nickName: "",
userName: "",
avatar: "",
role: 0,
token: "",
lookCount: 0,
articleCount: 0,
fansCount: 0,
followCount: 0,
place: ""
}
Message.success("用户注销成功")
// 注销成功后跳转,这里设置跳转到web页面,实际按照情况而定
router.push({name: "login"})
},
},
src\components\common\g_user_dropdown.vue【下拉菜单中添加处理注销登录的逻辑】
....
....
function handleSelect(val:string){
if (val === "logout"){
// 注销流程
store.userLogout()
return
}
// 其他选项跳转到相应的页面中去
router.push({
name:val
})
}
...
...
路由前置导航判断用户权限
src\router\index.ts【在路由中添加role字段,并在前置守卫中对角色进行角色校验判断】
import { createRouter, createWebHistory } from 'vue-router'
import NProgress from "nprogress";
import {userStorei} from "@/stores/user_store";
import {Message} from "@arco-design/web-vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
name: "home",
path: "/",
redirect: "/admin"
},
{
name: "web",
path: "/web",
component: () => import("@/views/web/index.vue")
},
{
name: "login",
path: "/login",
component: () => import("@/views/login/index.vue")
},
{
name: "admin",
path: "/admin",
component: () => import("@/views/admin/index.vue"),
meta: {
title: "首页",
role: [1, 2, 3]
},
children: [
{
name: "home",
path: "",
component: () => import("@/views/admin/home/index.vue")
},
{
name: "userCenter",
path: "user_Center",
children: [
{
name: "userinfo",
path: "user_info",
component: () => import("@/views/admin/user_center/index.vue"),
meta: {
title: "用户信息"
}
}
],
meta: {
title: "用户中心"
}
},
{
name: "userManage",
path: "user_Manage",
children: [
{
name: "userlist",
path: "user_list",
component: () => import("@/views/admin/user_manage/index.vue"),
meta: {
title: "用户列表",
role: [1]
}
}
],
meta: { title: "用户管理" }
},
{
name: "systemCenter",
path: "system_Center",
children: [
{
name: "systeminfo",
path: "system_info",
component: () => import("@/views/admin/system_manage/index.vue"),
meta: {
title: "系统管理",
}
}
],
meta: {
title: "系统信息",
role: [1]
}
},
]
}
],
})
router.beforeEach((to, from, next) => {
// 判断当前用户的角色,在不在列表里面
const store = userStorei()
if(to.meta.role){
if(!to.meta.role.includes(store.userInfo.role)){
// 不在role的列表中
Message.warning("权限不足")
return
}
}
NProgress.start();//开启进度条
next()
})
//当路由进入后:关闭进度条
router.afterEach(() => {
// 在即将进入新的页面组件前,关闭掉进度条
NProgress.done()//完成进度条
})
export default router
env.d.ts【给meta字段添加role类型声明】
/// <reference types="vite/client" />
// 给 RouteMeta 声明一个title类型,这样在声明对象时不会报警告,也不会飘红
import "vue-router"
declare module "ue-router"{
interface RouteMeta{
title:string,
role?:number[]
}
}
// 声明类型,继承自Record
export interface EnvMeta extends Record<string,string>{
VITE_SERVER_URL:string
VITE_SERVER_NAME:string
}
src\views\login\index.vue【登录成功后重定向】
...
...
async function pwdLogin(){
const val = await formRef.value.validate()
if(val) return
const res = await pwdLoginApi(form)
if (res.code){
Message.error(res.msg)
return
}
Message.success("登录成功")
// 如何获取用户信息 1. 直接解析 token 2. 调用户信息接口
userStore.saveUserInfo(res.data)
// 重定向
const redirect = route.query.redirect
if (redirect){
router.push(redirect as string)
return
}
router.push({
name:"admin"
})
}
...
...
mock的使用
【使用mock模拟后端数据-与api接口保持一致】
npm install -D @types/mockjs
npm install mockjs
package.json【配置npm启动mock的配置1】
...
...
"scripts": {
"dev": "vite",
"mock": "vite --mode mock",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
...
...
.env.mock【配置npm启动mock的配置2】<记得在.envt添加VITE_MOCK=false>
VITE_SERVERNAME='测试环境1'
VITE_SERVER_URL=http://127.0.0.1:8081
VITE_MOCK=true
src\mock\index.ts
import {userMock} from "@/mock/user_mock";
export function apiMock(){
// 拿到这个配置文件中的VITE_MOCK 如果是为true就启动mock,否则就不启动mock
const env = import.meta.env
if (env.VITE_MOCK !== "true"){
return
}
userMock()
}
src\mock\user_mock.ts
import {mock} from "mockjs";
import {type MockjsRequestOptions} from "mockjs"
export function userMock(){
mock(/.*?api\/user\/login/, function (options: MockjsRequestOptions){
// token 2028年十月过期
return {
code: 0,
data: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJkb3VibGUiLCJ1c2VyX2lkIjoxNSwicm9sZV9pZCI6MSwiZXhwIjoxODU0Njg5MzY3LCJpc3MiOiJkb3VibGVXZW4ifQ.c1kr2Nw8cRly-XvVBmwskbSb7OUnl0Q67DP1ujIRgt4",
msg: "成功"
}
})
mock(/.*?api\/user\/logout/, function (options: MockjsRequestOptions){
return {
code: 0,
data: "",
msg: "成功"
}
})
mock(/.*?api\/user\/base/, function (options: MockjsRequestOptions){
return mock({
"code": 0,
"data": {
"id": 15,
"created_at": "2026-01-12T16:24:53.826+08:00",
"username": "double",
"nickname": "back1",
"avatar": "/uploads/images001/74905ab1e9c16f37fab666eca89b56f2.png",
"abstract": "",
"register_source": 3,
"code_age": 1,
"role": 1,
"user_id": 15,
"like_tags": null,
"update_username_date": null,
"open_collect": true,
"open_follow": true,
"open_fans": true,
"home_style_id": 1,
"index_count": 0,
"email": "",
"usePassword": true
},
"msg": "成功"
})
})
}
src\main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import "@/assets/base.css"//引入初始化样式(放在最前面)
import "nprogress/nprogress.css";
import App from './App.vue'
import router from './router'
import "@/assets/iconfont.css" //引入iconfont图标
import ArcoVue from '@arco-design/web-vue'; //引入arco_design
import '@arco-design/web-vue/dist/arco.css';//引入arco_design 样式
import "@/assets/publick.less" //自定义样式
import ArcoVueIcon from '@arco-design/web-vue/es/icon'; //引入arco_design 图标
import {apiMock} from "@/mock"; //引入mock
const app = createApp(App)
apiMock() //启动mock的启动函数
app.use(createPinia())
app.use(router)
app.use(ArcoVue); //注册arco_design
app.use(ArcoVueIcon);//注册arco_design图标
app.mount('#app')
启动mock npm run dev
更多推荐



所有评论(0)