效果图
目录:
一、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
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'