项目搭建

创建项目

//====第一步创建项目========================
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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐