Vue 3 + TypeScript + Vite-目录02-src

107 阅读4分钟

效果图

image.png

目录:

image.png

一、api

1、api/model/user.ts

  export interface LoginParams {
    username:string;
    password:string;
  }

2、api/login.ts

import {Request} from '@/utils/request'
import {LoginParams} from '@/api/model/user.ts'

export const login = (data: typeof LoginParams)=>{
  return Request({
    method:'post',
    url: '/api/login',
    data
  })
}

export const logout = ()=>{
  return Request({
    method:'get',
    url: '/api/logout'
  })
}

3、api/user.ts

import {Request} from '@/utils/request.ts'

export const getUser = ()=>{
  return Request({
    method:'get',
    url: '/api/userInfo'
  })
}

export const getRouterInfo = ()=>{
  return Request({
    method:'get',
    url: '/api/routerInfo'
  })
}

二、components

1、components/ZcToggle.vue

<script lang="ts" setup>
import { withDefaults,defineProps,ref} from 'vue'

const props = withDefaults(defineProps<{
  defaultValue: boolean ,
  onresize:boolean
	}>(),{
	  defaultValue:true,
    onresize:false
	})

let isCollapsible = ref(props.defaultValue)

const onToggle = ()=>{
  if(isCollapsible.value){
    isCollapsible.value = !isCollapsible.value
  }else{
    isCollapsible.value = true
  }
}

// 是否需要根据视窗变更,而数据变更:菜单
if(props.onresize){
  window.onresize = () => {
    const clientWidth = document.documentElement.clientWidth
    isCollapsible.value = clientWidth<1024
  }
}

</script>

<template>
  <slot :isCollapsible="isCollapsible" :onToggle="onToggle"></slot>
</template>

三 、layout

image.png

1、layout/components/Navbar/index.vue

<!-- eslint-disable vue/multi-word-component-names -->
<template>
  <el-header class="navbar-container">
    <LeftNavbar />
    <div class="right-content">
      <el-avatar :src="src" :icon="UserFilled" />
      <el-dropdown @command="onCommand">
        <el-icon class="el-icon--right">
          <CaretBottom />
        </el-icon>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item v-for="(item,index) of dropOptions" :key="index" :disabled="item.disabled" :command="item.value">{{item.label}}</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </el-header>
</template>

<script lang="ts" setup>
import LeftNavbar from './LeftNavbar.vue'
import { ref,computed} from 'vue'
import { UserFilled } from '@element-plus/icons-vue'
import { CaretBottom } from '@element-plus/icons-vue'
import {useRouter} from 'vue-router'  
import {logout} from '@/api/login.ts'

const src = ref('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fthumbs.gfycat.com%2FCostlyEvenHydra-size_restricted.gif&refer=http%3A%2F%2Fthumbs.gfycat.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1673078839&t=f69323ab93068dd098c725c56528aa3d')

interface DropObj {
  value:string
  label:string
  disabled?:boolean
}

const router = useRouter()

const dropOptions = computed(()=>{
  let options = [
    {
        value:'Dashboard',
        label:'首页'
      },
      {
        value:'Login',
        label:'登出'
      }
    ] as DropObj[]
    const currentPath = router.currentRoute.value
    if(currentPath.name==='Dashboard'){
      options[0].disabled = true
    }
  return options
})

const onCommand =async (command: string)=>{
  if(command==='Login'){
    await logout().then((res:any)=>{
      if(res.data){
        localStorage.clear()
      }
    })
  }
  router.push({name:command})
}

</script>

<style lang="scss" scoped>
  .navbar-container{
    display: flex;
    justify-content: space-between;
    height: inherit;
    align-items: center;
    .right-content{
      img{
        cursor: pointer;
        width: 40px;
        height: 40px;
        border-radius: 10px;
      }
      .el-dropdown{
        color:var(--el-color-primary-light-9);
        vertical-align:bottom;
      }
    }
  }
</style>

2、layout/components/Navbar/LeftNavbar.vue

<template>
  <div class="container">
    <div class="left-content">
        <el-icon v-if="props.isCollapsible" @click="props.onToggle"><Fold /></el-icon>
        <el-icon v-if="!props.isCollapsible" @click="props.onToggle"><Expand /></el-icon>
    </div>
    <div class="right-content">
      <el-breadcrumb separator="/">
        <el-breadcrumb-item v-for="(item,index) of currentRouters" :key="index">{{item.meta.title||''}}</el-breadcrumb-item>
      </el-breadcrumb>
    </div>  
  </div>
</template>

<script lang="ts" setup>
import { Fold,Expand } from '@element-plus/icons-vue'
import { inject,ref,watch} from 'vue'
import {useRouter,RouteLocationMatched,RouteLocationNormalizedLoaded} from 'vue-router'  

const props = inject('fatherProps') as any

const { currentRoute } = useRouter();

const currentRouters = ref<RouteLocationMatched[]>([]);

watch(() => currentRoute.value,(route: RouteLocationNormalizedLoaded)=>{
  currentRouters.value = route.matched || []
},{
  immediate:true
})


</script>

<style lang="scss" scoped>
  .container{
    display:flex;
    align-items: center;
    .left-content{
      height: 1em;
      margin-right: 10px;
      cursor: pointer;
    }
  }
</style>

3、layout/components/Sidebar/index.vue


<template>
  <el-aside class="tac">
    <div :class="isCollapse?'header-logo el-menu--collapse':'header-logo'">logo</div>
      <el-menu
        :default-active="defaultActive"
        class="el-menu-vertical-demo"
        :collapse="isCollapse"
        @select="handleSelect"
      >
      <SideItem v-for="(item,index) of routers" :key="index" :route="item" />
      </el-menu>
  </el-aside>
</template>

<script lang="ts" setup>

import SideItem from './SideItem.vue'
import {useRouter} from 'vue-router'  
import { computed,inject} from 'vue'
import {useUserInfoStore} from '@/stores/userInfo.ts'

const store = useUserInfoStore()

const router= useRouter()

// 路由
const routers = store.routerList.filter((item:any)=>{
  return !item.hidden
})

const props = inject('fatherProps') as any

const isCollapse =computed(()=>{
  return props.isCollapsible
})


const defaultActive = computed(()=>{
  return router.currentRoute.value.name 
})

const handleSelect = (index:string)=>{
  router.push({name:index})
}

</script>

<style lang="scss" scoped>
.tac{
  text-align: center;
  .header-logo{
    line-height: 64px;
    color: white;
    font-weight: 600;
  }
  .el-menu {
    border-right: 0;
  }
  .el-menu-vertical-demo:not(.el-menu--collapse) {
    width: 220px;
    min-height: 400px;
  }
}
</style>

4、layout/components/Sidebar/SideItem.vue

<template>
  <div>
    <el-sub-menu v-if="route.children&&route.children.length>1" :index="route.name">
      <template #title>
        <el-icon v-if="isFirstLevel(route.path)"><location /></el-icon>
        <span class="title-label">{{route.meta&&route.meta.title||''}}</span>
      </template>
      <SideItem v-for="(item,index) of route.children" :key="index" :route="item" />
    </el-sub-menu>
    <el-menu-item v-else  :index="route.name"> 
      <el-icon v-if="isFirstLevel(route.path)"><icon-menu /></el-icon>
      <span>{{route.meta&&route.meta.title||''}}</span>
    </el-menu-item>
  </div>
</template>

<script lang="ts" setup>
import {
  Menu as IconMenu,
  Location
} from '@element-plus/icons-vue'
import {defineProps} from 'vue'

defineProps<{
  route: any
}>()

// 是否是顶层
const isFirstLevel = (path:string)=>{
  return path.includes('/')
}
</script>

<style lang="scss" scoped>

</style>

5、layout/components/TagsView/index.vue

<template>
  <div class="tag-container">
    <div class="left-container" ref="leftContainerRef">
      <el-tag
        v-for="(tag,index) in tagsView"
        :ref="e=>setTagListRef(e,index)"
        :data-set="'data'+index"
        :key="tag.name"
        class="mx-1"
        :closable="index!=0"
        size="large"
        @click="onJumpRoute(tag)"
        @close="onCloseTag(tag,index)"
        :type="currentTagType(tag)"
        effect="plain"
        disable-transitions
      >
      {{ tag.meta.title }}
    </el-tag>
  </div>
  <div class="right-container" type="flex">
    <el-dropdown :style="isShowMoreRoute?'':{opacity:0.01}" >
      <el-icon :color="'#213547'"><MoreFilled /></el-icon>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item v-for="(moreItem,moreIndex) of moreTagsView" :key="moreIndex" @click="onJumpRoute(moreItem)">
            <el-icon><Minus /></el-icon>{{ moreItem.meta.title }}
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
    <el-dropdown>
        <el-icon>
          <ArrowDownBold />
        </el-icon>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item @click="onCloseTag" >
            <el-icon><Minus /></el-icon>关闭全部标签页
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
  </div>
</template>

<script lang="ts" setup>
  import {ref,watch,nextTick,computed} from 'vue'
  import { useRouter } from "vue-router";
  import {useTagsViewStore} from '@/stores/tagsView.ts'
  import { MoreFilled,ArrowDownBold,Minus } from '@element-plus/icons-vue'
  import {isEmpty,uniqBy} from 'lodash'

  const store = useTagsViewStore()

  const tagsView = ref(store.tagsView as any)

  const router= useRouter()

  const currentRoute = router.currentRoute as any;

  // 获取选中状态
  const currentTagType = (tag:any)=>{
    return tag.name===currentRoute.value.name?'success':''
  }

   // 左侧容器dom
   let leftContainerRef = ref<null | HTMLElement>(null)

  // 左侧容器tag的dom
  const tagListRef = ref<any>({})

  // 左侧容器tag塞ref
  const setTagListRef = (e:any,index:number)=>{
    if(e){
      tagListRef.value[index] = e
    }
  }
  const isShowMoreRoute = ref(false)

  // // TODO : 明年再战
  watch(tagsView,(value)=>{
    if(value){
       // 容器总width
      const clientSumWidth:any = leftContainerRef.value?.clientWidth
      const scrollWidth:any = leftContainerRef.value?.scrollWidth
      isShowMoreRoute.value = scrollWidth>clientSumWidth
    }
  },{
    deep:true
  })

  const onCloseTag = (tag:any,index:Number)=>{
      store.deleteTag(index)
      // 如果删除的是当前的路由 tag跳转为last
      if(tag?.name===currentRoute.value.name){
        const updateRoute = tagsView.value[tagsView.value.length-1]
        router.push({name:updateRoute.name})
      }
      router.push({name:'Dashboard'})
      isInVisibleArea()
  }

  const onJumpRoute = (tag:any)=>{
    router.push({name:tag.name})
    isInVisibleArea(tag)
  }

   // 更多路由集合
   const moreTagsView = ref([] as any)

   // more路由,tag滚动到可视区域
   const scrollToView =async (tag:any)=>{
    await nextTick(()=>{
      const findIndex = tagsView.value.findIndex((item:any)=>item.name===tag.name)
      tagListRef.value[findIndex]?.$el.scrollIntoView()
    })
  }

  // 计算tag的width
  const countTagsWidth = (data:any)=>{
     data.forEach((item:any,index:number) => {
      item.meta.width = tagListRef.value[index]?.$el?.offsetWidth+5
    });
    return data
  } 

  // 计算溢出的tag
  const isInVisibleArea =async (value?:any)=>{
    if(isEmpty(tagsView?.value)) return
    if(value){
      await scrollToView(value)
    }
    // 容器总width
    const clientSumWidth:any = leftContainerRef.value?.clientWidth
    // 左边容器滚动的width(隐藏)
    const scrollLeftWidth = leftContainerRef.value?.scrollLeft

    // 容器中tag数据(每个tag添加width)
    const newTagsView = countTagsWidth(tagsView.value) || []

    let compareWidth = 0
    let scrollLeftIndex= 0 // left隐藏结束index
    let scrollRightIndex = 0  // right隐藏开始index

      for(let i = 0;i<newTagsView.length;i++){
        compareWidth= compareWidth+(newTagsView[i].meta.width)
        if(scrollLeftWidth&&compareWidth>scrollLeftWidth){
          scrollLeftIndex = i
            break
        }
      }
      compareWidth = 0
      for(let i = scrollLeftIndex;i<newTagsView.length;i++){
        compareWidth= compareWidth+(newTagsView[i].meta.width)
        if(compareWidth>clientSumWidth){
          scrollRightIndex = i
          break
        }
      }
      moreTagsView.value = []
      if(scrollLeftWidth){
        moreTagsView.value = scrollLeftIndex===0?[newTagsView[0]]:newTagsView.slice(0,scrollLeftIndex+1)
      }
      if(scrollRightIndex){
        moreTagsView.value = moreTagsView.value.concat(newTagsView.slice(scrollRightIndex))
      }
      moreTagsView.value = moreTagsView.value.filter((item:any)=>{
        return currentRoute.value.name!=item.name
      })
      moreTagsView.value = uniqBy(moreTagsView.value,'name')
    }

    // 监听当前路由
    watch(currentRoute, (value)=>{
      if(value){
        store.addTag(value)
        isInVisibleArea(value)
      }
    },{immediate:true,deep:true})

</script>

<style lang="scss" scoped>
  .tag-container{
    width:100%;
    border-top: 1px solid #d9d9d9;
    display: flex;
    justify-content:space-between;
    .left-container{
      padding:5px;
      display: flex;
      flex-wrap:nowrap;
      width: calc(100% - 70px );
      overflow-x:auto;
      .el-tag{
        scroll-margin: 5px;
      }
    }
    .right-container{
      display: flex;
      .el-icon{
        width:34px;
        height: inherit;
        cursor: pointer;
      }
      .el-icon:last-child{
        border-left: 1px solid #d9d9d9;
      }
    }
  }
  ::-webkit-scrollbar{
    height: 3px;
  }
</style>
<style lang="scss">
  .el-popper.is-pure{
      width:180px!important;
    }

</style>

6、layout/components/AppMain.vue

<template>
  <el-main >
    <div class="appMain-container">
      <router-view></router-view>
    </div>
  </el-main>
</template>

<style lang="scss" scoped>
.el-main{
  background-color: #f7f7f7;
  .appMain-container{
    height: 100%;
    background: white;
  }
}
</style>

7、layout/index.vue

<template>
  <el-container class="container">
    <ZcToggle :defaultValue="false" v-slot="item" :onresize="true" >
      <Sidebar :provideFn="provideFn(item)"/>
      <el-container class="right-container">
        <Navbar />
        <TagsView />
        <AppMain />
      </el-container>
    </ZcToggle>
  </el-container>
</template>

<script lang="ts" setup>
import Sidebar from './components/Sidebar/index.vue'
import Navbar from './components/Navbar/index.vue'
import TagsView from './components/TagsView/index.vue'
import AppMain from './components/AppMain.vue'
import ZcToggle from '@/components/ZcToggle.vue'
import { ref,provide} from 'vue'
import { Fn } from '@vueuse/shared'

interface ItemObj {
  isCollapsible:boolean;
  onToggle:Fn;
}

const fatherProps = ref({} as ItemObj)

provide('fatherProps',fatherProps.value)

const provideFn = (item:ItemObj)=>{
  fatherProps.value.isCollapsible = item.isCollapsible
  fatherProps.value.onToggle = item.onToggle
}

</script>

<style lang="scss" scoped>
  .container{
    height:100%;
    .el-aside{
      background: #414954;
      width: auto;
    }
    .right-container{
      display: flex;
      flex-direction: column;
    }
  }
</style>

四、pages

1、login/index.vue

<template>
 <div class="login-container">
    <div class="content">
      <h3>Hello</h3>
      <el-form
        ref="formRef"
        :model="formData"
        status-icon
        class="demo-ruleForm"
      >
        <el-form-item  prop="username">
          <el-input v-model="formData.username" autocomplete="off" placeholder="请输入邮箱或账号" @focus="updateTipsText(false)" />
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="formData.password"
            type="password"
            autocomplete="off"
            placeholder="密码"
            @focus="updateTipsText(false)"
          />
          <Transition>
            <div v-if="isShowTipsText" style="color:red">{{tipsText}}</div>
          </Transition>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="login" style="width:100%">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
 </div>
</template>

<script lang="ts" setup>
  import { reactive,ref,computed} from 'vue'
  import { useLoginInfoStore } from '@/stores/login.ts'
  
  import {useRouter} from 'vue-router'

  const router = useRouter()

  const store = useLoginInfoStore()

  const formData = reactive({
    username:'',
    password:''
  })

  const tipsText = computed(()=>{
    if(!formData.username){
      return '请输入邮箱或账号'
    }else if(!formData.password){
      return '请输入密码'
    }
    return ''
  })

  const isShowTipsText = ref(false)

  const updateTipsText = (data:boolean)=>{
    isShowTipsText.value = data
  }

   const login = async ()=>{
    if(tipsText.value) return updateTipsText(true)
    const data = formData
    const res = await store.getTokenAction(data)
    if(res?.data?.token){
      router.push('/')
    }
  }

</script>

<style lang="scss" scoped>
    .login-container{
      display: flex;
      justify-content: center;
      height: inherit;
      align-items: center;
      .content{
        width:300px ;
      }
      .v-enter-active,
      .v-leave-active {
        transition: opacity 0.5s ease;
      }

      .v-enter-from,
      .v-leave-to {
        opacity: 0;
      }
    }
</style>

五、router

index.vue

import { createWebHistory, createRouter } from "vue-router";

import Layout from "@/layout/index.vue"

export const routes = [
  {
    path: "/",
    redirect:'/dashboard',
    name:'Dashboard',
    component: Layout,
    meta: { title: '首页' },
    children:[
      {
        path: "dashboard",
        name: "Dashboard",
        component: () => import('@/pages/dashboard/index.vue'),
        meta: { title: '首页1' },
      }
    ]
  },
  {
    path: "/login",
    name: "Login",
    component: () => import('@/pages/login/index.vue'),
    hidden:true
  }
];

export const errorRoute = [
  {
    path: '/:pathMatch(.*)*',
    name: '404',
    hidden: true,
    component: () => import('@/pages/404/index.vue'),
    meta: { title: 'P404' }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

六、stores

1、stores/login.ts

import { defineStore } from 'pinia'
import { ref} from 'vue'

import {login} from '@/api/login'
import {LoginParams} from '@/api/model/user.ts'

export const useLoginInfoStore = defineStore('token', () => {
  
  const token = ref('')

  async function getTokenAction(data:typeof LoginParams ) {
    const res = await login(data)
    if(res){
      localStorage.setItem('token',res.data.token)
      token.value = res.data.token
      return res
    }
  }

  return { token, getTokenAction }
})

2、stores/tagsView.ts

import { defineStore } from 'pinia'
import { ref} from 'vue'

import {login} from '@/api/login'
import {LoginParams} from '@/api/model/user.ts'

export const useLoginInfoStore = defineStore('token', () => {
  
  const token = ref('')

  async function getTokenAction(data:typeof LoginParams ) {
    const res = await login(data)
    if(res){
      localStorage.setItem('token',res.data.token)
      token.value = res.data.token
      return res
    }
  }

  return { token, getTokenAction }
})

3、stores/userInfo.ts

import { defineStore } from 'pinia'
import { ref} from 'vue'

import {login} from '@/api/login'
import {LoginParams} from '@/api/model/user.ts'

export const useLoginInfoStore = defineStore('token', () => {
  
  const token = ref('')

  async function getTokenAction(data:typeof LoginParams ) {
    const res = await login(data)
    if(res){
      localStorage.setItem('token',res.data.token)
      token.value = res.data.token
      return res
    }
  }

  return { token, getTokenAction }
})

七、utils

1、utils/dealRouter.ts

// 处理后台返回的路由数据,列:路由匹配对应文件
import Layout from "@/layout/index.vue"

interface RouteObj {
  path:string
  name:string
  redirect?:string
  component:any,
  meta?:MetaObj
  children?: any
  hidden?:boolean
}

interface MetaObj {
  title:string
  icon?:string
}

export const dealAsyncRouter = (data:RouteObj[])=>{
  let routers = dealRouter(data)
  return routers
}

const dealRouter = (data:RouteObj[])=>{
  let routers = data
  routers.forEach(item=>{
    if(item.component==='Layout'){
      item.component = Layout
    }else if(item.component){
      item.component = dealFilePath(item.component)
    }
    if(item.children){
      item.children = dealRouter(item.children)
    }
  })
  return routers
}

// 处理组件引入路径
const dealFilePath = (data:string)=>{
  // return () =>import(/* @vite-ignore */`/src/pages${data}.vue`) 
  return () =>import(/* @vite-ignore */`/src/pages${data}.vue`) 
}

2、utils/request.ts

import axios from 'axios'
import { ElMessage , ElLoading } from 'element-plus'
const ConfigBaseURL =import.meta.env.VITE_APP_BASE_URL //默认路径,这里也可以使用env来判断环境


let loadingInstance = ElLoading.service({}) //这里是loading
loadingInstance.close()


//使用create方法创建axios实例
export const Request = axios.create({
  timeout: 5000, // 请求超时时间
  baseURL: ConfigBaseURL,
  headers: {
    'Content-Type':'application/json;charset=UTF-8'
  }
})

// 添加请求拦截器
Request.interceptors.request.use((config:any) => {
  const token = localStorage.getItem('token')
  if(token){
    config.headers.auth = token
  }
  loadingInstance = ElLoading.service({
    lock: true,
    text: 'loading...'
  })
  return config
})


// 添加响应拦截器
Request.interceptors.response.use(response => {
  loadingInstance.close()
  // console.log(response)
  return response.data
}, error => {
  console.log('TCL: error', error)
  const msg = error.Message !== undefined ? error.Message : ''
  ElMessage({
    message: '网络错误' + msg,
    type: 'error',
    duration: 3 * 1000
  })
  loadingInstance.close()
  return Promise.reject(error)
})

八、App.vue

<template>
  <router-view />
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

九、main.ts

import { createApp } from 'vue'
import './styles/index.scss'
import App from './App.vue'
import router from './router/index'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import { createPinia } from 'pinia'
import '@/permission'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.use(router)
app.use(ElementPlus)

app.mount('#app')

十、permission.ts

import router from './router'
import {useUserInfoStore} from '@/stores/userInfo.ts'
import {isEmpty} from 'lodash'
// 进度条
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

// 路由加载前
router.beforeEach(async (to:any, from:any, next:any) => {
  NProgress.start()

  const store = useUserInfoStore()
  const token = localStorage.getItem('token')
  if(token){
    if(to.path==='/login'){
      next({path:'/'})
    }else{
      // 有无路由判断
      if(isEmpty(store.userInfo)){
        await store.getUserRouterInfoAction()
        if(!isEmpty(store.routerList)){
          store.routerList.forEach((item:any)=>{
            router.addRoute(item)
          })
          // 如果 addRoutes 并未完成,路由守卫会一层一层的执行执行,直到 addRoutes 完成,找到对应的路由
          // replace: true=>告诉VUE本次操作后,不能通过浏览器后退按钮,返回前一个路由
          next({...to, replace: true})
        }
      }
      next()
    }
  }else{
    if(to.path==='/login'){
      next()
    }else{
      next({path:'/login'})
    }
  }
})

router.afterEach(()=>{
  NProgress.done()
})

十一、styles

1、styles/element-ui.scss

import {Request} from '@/utils/request.ts'

export const getUser = ()=>{
  return Request({
    method:'get',
    url: '/api/userInfo'
  })
}

export const getRouterInfo = ()=>{
  return Request({
    method:'get',
    url: '/api/routerInfo'
  })
}

2、styles/index.scss


:root {
  font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 24px;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

.card {
  padding: 2em;
}
::-webkit-scrollbar{
  background-color: #000;
}
::-webkit-scrollbar-track {
background-color: #f1f1f1;
} 
::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
} 

.el-dropdown__popper.el-popper{
 
  .el-dropdown-menu{
    background:#ffffff ;
    .el-dropdown-menu__item {
        color: #213547;
    }
    .el-dropdown-menu__item:not(.is-disabled):focus{
      background:#ffffff ;
      color: var(--el-color-primary);
    }
  }
}
.el-popper.is-light .el-popper__arrow::before{
  background:#f1f1f1!important;
}

#app {
  width: 100%;
  height: 100vh;
  /* padding: 2rem; */
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}

// *{
//   scrollbar-face-color:#fff;/*滑块颜色*/ 
//     scrollbar-arrow-color:#000; /*箭头颜色*/
//     scrollbar-shadow-color:#000000; /*滑块边线颜色*/
//     scrollbar-track-color:#dde3fa; /*滑轨颜色*/
//   }
 

@import './element-ui.scss'