mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-10-31 00:57:11 +08:00
first commit
This commit is contained in:
32
src/layout/LayoutDefault.vue
Normal file
32
src/layout/LayoutDefault.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<a-layout class="layout layout-default">
|
||||
<Asider></Asider>
|
||||
<a-layout class="layout-default-right">
|
||||
<Header></Header>
|
||||
<Tabs></Tabs>
|
||||
<Main></Main>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Asider from './components/Asider/index.vue'
|
||||
import Header from './components/Header/index.vue'
|
||||
import Main from './components/Main.vue'
|
||||
import Tabs from './components/Tabs/index.vue'
|
||||
|
||||
defineOptions({ name: 'LayoutDefault' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-default {
|
||||
flex-direction: row;
|
||||
&-right {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
178
src/layout/LayoutMix.vue
Normal file
178
src/layout/LayoutMix.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div class="layout-mix">
|
||||
<section
|
||||
v-if="isDesktop"
|
||||
class="layout-mix-left"
|
||||
:class="{ 'app-menu-dark': appStore.menuDark }"
|
||||
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
|
||||
>
|
||||
<Logo :collapsed="appStore.menuCollapse"></Logo>
|
||||
<Menu :menus="leftMenus" :menu-style="{ width: '200px', flex: 1 }"></Menu>
|
||||
</section>
|
||||
|
||||
<section class="layout-mix-right">
|
||||
<header class="header">
|
||||
<MenuFoldBtn></MenuFoldBtn>
|
||||
<a-menu
|
||||
v-if="isDesktop"
|
||||
mode="horizontal"
|
||||
:selected-keys="activeMenu"
|
||||
:auto-open-selected="false"
|
||||
:trigger-props="{ animationName: 'slide-dynamic-origin' }"
|
||||
@menu-item-click="onMenuItemClick"
|
||||
>
|
||||
<a-menu-item v-for="item in topMenus" :key="item.path">
|
||||
<template #icon>
|
||||
<GiSvgIcon
|
||||
v-if="getMenuIcon(item, 'svgIcon')"
|
||||
:name="getMenuIcon(item, 'svgIcon')"
|
||||
:size="24"
|
||||
></GiSvgIcon>
|
||||
<template v-else>
|
||||
<component v-if="getMenuIcon(item, 'svgIcon')" :is="getMenuIcon(item, 'icon')"></component>
|
||||
</template>
|
||||
</template>
|
||||
<span>{{ item.meta?.title || item.children?.[0]?.meta?.title || '' }}</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<HeaderRightBar></HeaderRightBar>
|
||||
</header>
|
||||
|
||||
<Tabs></Tabs>
|
||||
<Main></Main>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Main from './components/Main.vue'
|
||||
import Tabs from './components/Tabs/index.vue'
|
||||
import Menu from './components/Menu/index.vue'
|
||||
import HeaderRightBar from './components/HeaderRightBar/index.vue'
|
||||
import Logo from './components/Logo.vue'
|
||||
import MenuFoldBtn from './components/MenuFoldBtn.vue'
|
||||
import { useAppStore, useRouteStore } from '@/stores'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
import { searchTree } from 'xe-utils'
|
||||
import { filterTree } from '@/utils'
|
||||
import { useDevice } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'LayoutMix' })
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const routeStore = useRouteStore()
|
||||
const { isDesktop } = useDevice()
|
||||
// 过滤是菜单的路由
|
||||
const menuRoutes = filterTree(routeStore.routes, (i) => i.meta?.hidden === false)
|
||||
|
||||
// 顶部一级菜单
|
||||
const topMenus = ref<RouteRecordRaw[]>([])
|
||||
topMenus.value = JSON.parse(JSON.stringify(menuRoutes))
|
||||
console.log('topMenus', toRaw(topMenus.value))
|
||||
|
||||
const getMenuIcon = (item: RouteRecordRaw, key: 'svgIcon' | 'icon') => {
|
||||
return item.meta?.[key] || item.children?.[0].meta?.[key]
|
||||
}
|
||||
|
||||
const onMenuItemClick = (key: string) => {
|
||||
if (isExternal(key)) {
|
||||
window.open(key)
|
||||
return
|
||||
}
|
||||
setTimeout(() => getLeftMenus(key))
|
||||
const obj = topMenus.value.find((i) => i.path === key)
|
||||
if (obj && obj.redirect === 'noRedirect') return
|
||||
router.push({ path: key })
|
||||
}
|
||||
|
||||
// 克隆是菜单的路由
|
||||
const cloneMenuRoutes: RouteRecordRaw[] = JSON.parse(JSON.stringify(menuRoutes))
|
||||
// 顶部一级菜单选中的
|
||||
const activeMenu = ref<string[]>([])
|
||||
// 左侧的菜单
|
||||
const leftMenus = ref<RouteRecordRaw[]>([])
|
||||
// 获取左侧菜单
|
||||
const getLeftMenus = (key: string) => {
|
||||
const arr = searchTree(cloneMenuRoutes, (i) => i.path === key, { children: 'children' })
|
||||
const rootPath = arr.length ? arr[0].path : ''
|
||||
const obj = cloneMenuRoutes.find((i) => i.path === rootPath)
|
||||
activeMenu.value = obj ? [obj.path] : ['']
|
||||
leftMenus.value = obj ? (obj.children as RouteRecordRaw[]) : []
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
(newPath) => {
|
||||
nextTick(() => {
|
||||
getLeftMenus(newPath)
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-menu-pop) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.arco-menu.arco-menu-vertical.arco-menu-collapsed) {
|
||||
// Menu菜单组件修改
|
||||
.arco-menu-icon {
|
||||
margin-right: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.arco-menu-has-icon {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
.arco-menu-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-menu-horizontal) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.arco-menu-inner {
|
||||
padding-left: 0;
|
||||
.arco-menu-overflow-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-mix {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
&-left {
|
||||
border-right: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
&-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0 $padding;
|
||||
height: 56px;
|
||||
color: var(--color-text-1);
|
||||
background: var(--color-bg-1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
74
src/layout/components/Asider/index.vue
Normal file
74
src/layout/components/Asider/index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isDesktop"
|
||||
class="asider"
|
||||
:class="{ 'app-menu-dark': appStore.menuDark }"
|
||||
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
|
||||
>
|
||||
<Logo :collapsed="appStore.menuCollapse"></Logo>
|
||||
<a-layout-sider
|
||||
class="menu"
|
||||
collapsible
|
||||
breakpoint="xl"
|
||||
hide-trigger
|
||||
:width="220"
|
||||
:collapsed="appStore.menuCollapse"
|
||||
@collapse="handleCollapse"
|
||||
>
|
||||
<a-scrollbar outer-class="h-full" style="height: 100%; overflow: auto">
|
||||
<Menu></Menu>
|
||||
</a-scrollbar>
|
||||
</a-layout-sider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/stores'
|
||||
import Menu from '../Menu/index.vue'
|
||||
import Logo from '../Logo.vue'
|
||||
import { useDevice } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'Asider' })
|
||||
const appStore = useAppStore()
|
||||
const { isDesktop } = useDevice()
|
||||
|
||||
const handleCollapse = (isCollapsed: boolean) => {
|
||||
appStore.menuCollapse = isCollapsed
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-menu.arco-menu-vertical.arco-menu-collapsed) {
|
||||
// Menu菜单组件修改
|
||||
.arco-menu-icon {
|
||||
margin-right: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.arco-menu-has-icon {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
.arco-menu-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-layout-sider-children) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.asider {
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--color-border-2);
|
||||
box-sizing: border-box;
|
||||
color: var(--color-text-1);
|
||||
background-color: var(--color-bg-1);
|
||||
.menu {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
src/layout/components/Header/index.vue
Normal file
47
src/layout/components/Header/index.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<a-layout-header class="header">
|
||||
<section class="fold-btn-wrapper">
|
||||
<MenuFoldBtn></MenuFoldBtn>
|
||||
</section>
|
||||
<a-row align="center" class="h-full header-right">
|
||||
<a-col :xs="0" :md="10" :lg="10" :xl="12" :xxl="12">
|
||||
<Breadcrumb></Breadcrumb>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="14" :lg="14" :xl="12" :xxl="12">
|
||||
<a-row justify="end" align="center">
|
||||
<HeaderRightBar></HeaderRightBar>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HeaderRightBar from '../HeaderRightBar/index.vue'
|
||||
import MenuFoldBtn from '../MenuFoldBtn.vue'
|
||||
|
||||
defineOptions({ name: 'Header' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.arco-dropdown-open .arco-icon-down {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.header-right {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin-left: $padding;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-layout-header {
|
||||
padding: 0 $padding;
|
||||
height: 56px;
|
||||
background: var(--color-bg-1);
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
</style>
|
||||
56
src/layout/components/HeaderRightBar/Message.vue
Normal file
56
src/layout/components/HeaderRightBar/Message.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="message">
|
||||
<a-tabs default-active-key="1">
|
||||
<a-tab-pane key="1">
|
||||
<template #title>通知(1)</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2">
|
||||
<template #title>关注(1)</template>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3">
|
||||
<template #title>待办(2)</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<section>
|
||||
<a-comment
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
:author="item.name"
|
||||
:content="item.content"
|
||||
:datetime="item.datetime"
|
||||
>
|
||||
<template #actions></template>
|
||||
<template #avatar>
|
||||
<a-avatar><img :src="item.avatar" /></a-avatar>
|
||||
</template>
|
||||
</a-comment>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'Message' })
|
||||
const list = [
|
||||
{
|
||||
name: 'Socrates',
|
||||
datetime: '1小时之前',
|
||||
content: 'Comment body content.',
|
||||
avatar: 'https://lolicode.gitee.io/scui-doc/demo/img/avatar2.gif'
|
||||
},
|
||||
{
|
||||
name: '木木糖醇',
|
||||
datetime: '2小时之前',
|
||||
content: '关注了你',
|
||||
avatar: 'https://s1.ax1x.com/2022/06/14/XhteeO.jpg'
|
||||
},
|
||||
{
|
||||
name: '徐欣',
|
||||
datetime: '2个半小时之前',
|
||||
content: '收藏了你的文章',
|
||||
avatar: 'https://s1.ax1x.com/2022/06/14/XhtSwF.jpg'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
158
src/layout/components/HeaderRightBar/SettingDrawer.vue
Normal file
158
src/layout/components/HeaderRightBar/SettingDrawer.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible" title="项目配置" width="300px" unmount-on-close :footer="false">
|
||||
<a-space :size="15" direction="vertical" fill>
|
||||
<a-divider orientation="center">系统布局</a-divider>
|
||||
<a-row justify="center">
|
||||
<a-space>
|
||||
<a-badge>
|
||||
<template #content>
|
||||
<icon-check-circle-fill
|
||||
v-if="appStore.layout === 'left'"
|
||||
style="color: rgb(var(--success-6))"
|
||||
:size="16"
|
||||
></icon-check-circle-fill>
|
||||
</template>
|
||||
<LayoutItem mode="left" @click="appStore.layout = 'left'"></LayoutItem>
|
||||
<p class="layout-text">默认布局</p>
|
||||
</a-badge>
|
||||
<a-badge>
|
||||
<template #content>
|
||||
<icon-check-circle-fill
|
||||
v-if="appStore.layout === 'mix'"
|
||||
:size="16"
|
||||
style="color: rgb(var(--success-6))"
|
||||
></icon-check-circle-fill>
|
||||
</template>
|
||||
<LayoutItem mode="mix" @click="appStore.layout = 'mix'"></LayoutItem>
|
||||
<p class="layout-text">混合布局</p>
|
||||
</a-badge>
|
||||
</a-space>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="center">系统主题</a-divider>
|
||||
<a-row justify="center">
|
||||
<ColorPicker
|
||||
theme="dark"
|
||||
:color="appStore.themeColor"
|
||||
:sucker-hide="true"
|
||||
:colors-default="defaultColorList"
|
||||
@changeColor="changeColor"
|
||||
></ColorPicker>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="center">界面显示</a-divider>
|
||||
|
||||
<a-descriptions :column="1" :align="{ value: 'right' }" :value-style="{ paddingRight: 0 }">
|
||||
<a-descriptions-item label="页签显示">
|
||||
<a-switch v-model="appStore.tab" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="页签风格">
|
||||
<a-select
|
||||
v-model="appStore.tabMode"
|
||||
placeholder="请选择"
|
||||
:options="tabModeList"
|
||||
:disabled="!appStore.tab"
|
||||
:trigger-props="{ autoFitPopupMinWidth: true }"
|
||||
:style="{ width: '120px' }"
|
||||
>
|
||||
</a-select>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="动画显示">
|
||||
<a-switch v-model="appStore.animate" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="动画显示">
|
||||
<a-select
|
||||
v-model="appStore.animateMode"
|
||||
placeholder="请选择"
|
||||
:options="animateModeList"
|
||||
:disabled="!appStore.animate"
|
||||
:style="{ width: '120px' }"
|
||||
>
|
||||
</a-select>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="深色菜单">
|
||||
<a-switch v-model="appStore.menuDark" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="手风琴效果">
|
||||
<a-switch v-model="appStore.menuAccordion" />
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-space>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/stores'
|
||||
import { ColorPicker } from 'vue-color-kit'
|
||||
import 'vue-color-kit/dist/vue-color-kit.css'
|
||||
import LayoutItem from './components/LayoutItem.vue'
|
||||
|
||||
defineOptions({ name: 'SettingDrawer' })
|
||||
const appStore = useAppStore()
|
||||
const visible = ref(false)
|
||||
|
||||
const tabModeList: App.TabItem[] = [
|
||||
{ label: '卡片', value: 'card' },
|
||||
{ label: '间隔卡片', value: 'card-gutter' },
|
||||
{ label: '圆角', value: 'rounded' }
|
||||
]
|
||||
|
||||
const animateModeList: App.AnimateItem[] = [
|
||||
{ label: '默认', value: 'zoom-fade' },
|
||||
{ label: '滑动', value: 'fade-slide' },
|
||||
{ label: '渐变', value: 'fade' },
|
||||
{ label: '底部滑出', value: 'fade-bottom' },
|
||||
{ label: '缩放消退', value: 'fade-scale' }
|
||||
]
|
||||
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
// 默认显示的主题色列表
|
||||
const defaultColorList = [
|
||||
'#165DFF',
|
||||
'#409EFF',
|
||||
'#18A058',
|
||||
'#2d8cf0',
|
||||
'#007AFF',
|
||||
'#5ac8fa',
|
||||
'#5856D6',
|
||||
'#536dfe',
|
||||
'#9c27b0',
|
||||
'#AF52DE',
|
||||
'#0096c7',
|
||||
'#00C1D4',
|
||||
'#43a047',
|
||||
'#e53935',
|
||||
'#f4511e',
|
||||
'#6d4c41'
|
||||
]
|
||||
|
||||
type ColorObj = {
|
||||
hex: string
|
||||
hsv: { h: number; s: number; v: number }
|
||||
rgba: { r: number; g: number; b: number; a: number }
|
||||
}
|
||||
|
||||
// 改变主题色
|
||||
const changeColor = (colorObj: ColorObj) => {
|
||||
if (!/^#[0-9A-Za-z]{6}/.test(colorObj.hex)) return
|
||||
appStore.setThemeColor(colorObj.hex)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-descriptions-item-label-block) {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.layout-text {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: var(--color-text-2);
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="layout-item" :class="`layout-item-${mode}`" @click="emit('click')"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
mode: 'left' | 'top' | 'mix'
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-item {
|
||||
width: 60px;
|
||||
height: 50px;
|
||||
background-color: var(--color-fill-3);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
background-color: rgb(var(--gray-9));
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background-color: rgb(var(--gray-9));
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
&-left {
|
||||
&::before {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&-top {
|
||||
&::after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&-mix {
|
||||
&::before,
|
||||
&::after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
125
src/layout/components/HeaderRightBar/index.vue
Normal file
125
src/layout/components/HeaderRightBar/index.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<a-row justify="end" align="center">
|
||||
<a-space size="medium">
|
||||
<!-- 项目配置 -->
|
||||
<a-tooltip content="项目配置" position="bl">
|
||||
<a-button size="mini" class="gi_hover_btn" @click="SettingDrawerRef?.open">
|
||||
<template #icon>
|
||||
<icon-settings :size="18" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 消息通知 -->
|
||||
<a-popover position="bottom" trigger="click">
|
||||
<a-badge :count="9" dot>
|
||||
<a-button size="mini" class="gi_hover_btn">
|
||||
<template #icon>
|
||||
<icon-notification :size="18" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-badge>
|
||||
<template #content>
|
||||
<Message></Message>
|
||||
</template>
|
||||
</a-popover>
|
||||
|
||||
<!-- 全屏切换组件 -->
|
||||
<a-tooltip v-if="!isMobile()" content="全屏切换" position="bottom">
|
||||
<a-button size="mini" class="gi_hover_btn" @click="toggle">
|
||||
<template #icon>
|
||||
<icon-fullscreen :size="18" v-if="!isFullscreen" />
|
||||
<icon-fullscreen-exit :size="18" v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 暗黑模式切换 -->
|
||||
<a-tooltip content="主题切换" position="bottom">
|
||||
<GiThemeBtn></GiThemeBtn>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 管理员账户 -->
|
||||
<a-dropdown trigger="hover">
|
||||
<a-row align="center" :wrap="false" class="user">
|
||||
<!-- 管理员头像 -->
|
||||
<a-avatar :size="32">
|
||||
<img :src="userStore.avatar" alt="avatar" />
|
||||
</a-avatar>
|
||||
<span class="username">{{ userStore.name }}</span>
|
||||
<icon-down />
|
||||
</a-row>
|
||||
<template #content>
|
||||
<a-doption @click="toUser">
|
||||
<span>账号管理</span>
|
||||
</a-doption>
|
||||
<a-divider :margin="0" />
|
||||
<a-doption @click="logout">
|
||||
<span>退出登录</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</a-row>
|
||||
|
||||
<SettingDrawer ref="SettingDrawerRef"></SettingDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Modal } from '@arco-design/web-vue'
|
||||
import { useUserStore } from '@/stores'
|
||||
import SettingDrawer from './SettingDrawer.vue'
|
||||
import Message from './Message.vue'
|
||||
import { isMobile } from '@/utils'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
|
||||
defineOptions({ name: 'HeaderRight' })
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const SettingDrawerRef = ref<InstanceType<typeof SettingDrawer>>()
|
||||
|
||||
// 跳转基本信息
|
||||
const toUser = () => {
|
||||
router.push('/setting/profile')
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const logout = () => {
|
||||
Modal.warning({
|
||||
title: '提示',
|
||||
content: '确认退出登录?',
|
||||
hideCancel: false,
|
||||
closable: true,
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
await userStore.logout()
|
||||
router.replace('/login')
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.arco-dropdown-open .arco-icon-down {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.user {
|
||||
cursor: pointer;
|
||||
color: var(--color-text-1);
|
||||
.username {
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.arco-icon-down {
|
||||
transition: all 0.3s;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
69
src/layout/components/Logo.vue
Normal file
69
src/layout/components/Logo.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<section class="system-logo" :class="{ collapsed: props.collapsed }" @click="toHome">
|
||||
<img class="logo" src="@/assets/images/logo.svg" />
|
||||
<span class="system-name">ContiNew Admin</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
collapsed: false
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 跳转首页
|
||||
const toHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.system-logo {
|
||||
height: 56px;
|
||||
padding: 0 12px;
|
||||
color: var(--color-text-1);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
&.collapsed {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// .logo {
|
||||
// width: 24px;
|
||||
// height: 24px;
|
||||
// }
|
||||
.system-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.system-name {
|
||||
padding-left: 10px;
|
||||
white-space: nowrap;
|
||||
transition: color 0.3s;
|
||||
&:hover {
|
||||
color: $color-theme !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
src/layout/components/Main.vue
Normal file
41
src/layout/components/Main.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<a-layout class="main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition :name="transitionName(route)" mode="out-in" appear>
|
||||
<keep-alive :include="(tabsStore.cacheList as string[])">
|
||||
<component :is="Component" :key="route.matched?.[1]?.path" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { useAppStore, useTabsStore } from '@/stores'
|
||||
|
||||
defineOptions({ name: 'Main' })
|
||||
const appStore = useAppStore()
|
||||
const tabsStore = useTabsStore()
|
||||
|
||||
// 过渡动画
|
||||
const transitionName = computed(() => {
|
||||
return function (route: RouteLocationNormalizedLoaded) {
|
||||
if (route?.matched?.[1]?.meta?.animation === false) {
|
||||
return ''
|
||||
}
|
||||
return appStore.transitionName
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
15
src/layout/components/Menu/MenuIcon.vue
Normal file
15
src/layout/components/Menu/MenuIcon.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<GiSvgIcon v-if="props.svgIcon" :name="props.svgIcon" :size="18"></GiSvgIcon>
|
||||
<component v-else-if="props.icon" :is="props.icon" style="height: 18px; width: 18px"></component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
svgIcon?: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
70
src/layout/components/Menu/MenuItem.vue
Normal file
70
src/layout/components/Menu/MenuItem.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<template v-if="!item.meta?.hidden">
|
||||
<a-menu-item
|
||||
v-if="
|
||||
isOneShowingChild &&
|
||||
(!onlyOneChild?.children || onlyOneChild?.meta?.noShowingChildren) &&
|
||||
!item?.meta?.alwaysShow
|
||||
"
|
||||
v-bind="attrs"
|
||||
:key="onlyOneChild?.path"
|
||||
>
|
||||
<template #icon>
|
||||
<MenuIcon
|
||||
:svg-icon="onlyOneChild?.meta?.svgIcon || item?.meta?.svgIcon"
|
||||
:icon="onlyOneChild?.meta?.icon || item?.meta?.icon"
|
||||
></MenuIcon>
|
||||
</template>
|
||||
<span>{{ onlyOneChild?.meta?.title }}</span>
|
||||
</a-menu-item>
|
||||
|
||||
<a-sub-menu v-else v-bind="attrs" :key="item.path" :title="item?.meta?.title">
|
||||
<template #icon>
|
||||
<MenuIcon :svg-icon="item?.meta?.svgIcon" :icon="item?.meta?.icon"></MenuIcon>
|
||||
</template>
|
||||
<MenuItem v-for="child in item.children" :key="child.path" :item="child"></MenuItem>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import MenuIcon from './MenuIcon.vue'
|
||||
|
||||
defineOptions({ name: 'MenuItem' })
|
||||
const attrs = useAttrs()
|
||||
|
||||
interface Props {
|
||||
item: RouteRecordRaw
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
// 如果hidden: false那么代表这个路由项显示在左侧菜单栏中
|
||||
// 如果props.item的子项chidren只有一个hidden: false的子元素, 那么onlyOneChild就表示这个子元素
|
||||
const onlyOneChild = ref<RouteRecordRaw | null>(null)
|
||||
const isOneShowingChild = ref(false)
|
||||
|
||||
const handleFunction = () => {
|
||||
const chidrens = props.item?.children?.length ? props.item.children : []
|
||||
// 判断是否只有一个显示的子项
|
||||
const showingChildrens = chidrens.filter((i) => i.meta?.hidden === false)
|
||||
if (showingChildrens.length) {
|
||||
// 保存子项最后一个hidden: false的元素
|
||||
onlyOneChild.value = showingChildrens[showingChildrens.length - 1]
|
||||
}
|
||||
|
||||
// 当只有一个要显示子路由时, 默认显示该子路由器
|
||||
if (showingChildrens.length === 1) {
|
||||
isOneShowingChild.value = true
|
||||
}
|
||||
|
||||
// 如果没有要显示的子路由, 则显示父路由
|
||||
if (showingChildrens.length === 0) {
|
||||
onlyOneChild.value = { ...props.item, meta: { ...props.item.meta, noShowingChildren: true } } as any
|
||||
isOneShowingChild.value = true
|
||||
}
|
||||
}
|
||||
|
||||
handleFunction()
|
||||
</script>
|
||||
87
src/layout/components/Menu/index.vue
Normal file
87
src/layout/components/Menu/index.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<a-menu
|
||||
:mode="mode"
|
||||
:selected-keys="activeMenu"
|
||||
:auto-open-selected="autoOpenSelected"
|
||||
:accordion="appStore.menuAccordion"
|
||||
:breakpoint="appStore.layout === 'mix' ? 'xl' : undefined"
|
||||
:trigger-props="{ animationName: 'slide-dynamic-origin' }"
|
||||
:collapsed="!isDesktop ? false : appStore.menuCollapse"
|
||||
@menu-item-click="onMenuItemClick"
|
||||
@collapse="onCollapse"
|
||||
:style="menuStyle"
|
||||
>
|
||||
<MenuItem v-for="(route, index) in sidebarRoutes" :key="route.path + index" :item="route"></MenuItem>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore, useRouteStore } from '@/stores'
|
||||
import MenuItem from './MenuItem.vue'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { useDevice } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'Menu' })
|
||||
const emit = defineEmits<{
|
||||
(e: 'menuItemClickAfter'): void
|
||||
}>()
|
||||
|
||||
interface Props {
|
||||
menus?: RouteRecordRaw[]
|
||||
menuStyle?: CSSProperties
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const { isDesktop } = useDevice()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const routeStore = useRouteStore()
|
||||
const sidebarRoutes = computed(() => (props.menus ? props.menus : routeStore.routes))
|
||||
// console.log('sidebarRoutes', sidebarRoutes.value)
|
||||
|
||||
// 菜单垂直模式/水平模式
|
||||
const mode = computed(() => {
|
||||
if (!['left', 'mix'].includes(appStore.layout)) {
|
||||
return 'horizontal'
|
||||
} else {
|
||||
return 'vertical'
|
||||
}
|
||||
})
|
||||
|
||||
// 是否默认展开选中的菜单
|
||||
const autoOpenSelected = computed(() => {
|
||||
return ['left', 'mix'].includes(appStore.layout);
|
||||
})
|
||||
|
||||
// 当前页面激活菜单路径,先从路由里面找
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route
|
||||
if (meta?.activeMenu) {
|
||||
return [meta.activeMenu]
|
||||
}
|
||||
return [path]
|
||||
})
|
||||
|
||||
// 菜单项点击事件
|
||||
const onMenuItemClick = (key: string) => {
|
||||
if (isExternal(key)) {
|
||||
window.open(key)
|
||||
return
|
||||
}
|
||||
router.push({ path: key })
|
||||
emit('menuItemClickAfter')
|
||||
}
|
||||
|
||||
// 折叠状态改变时触发
|
||||
const onCollapse = (collapsed: boolean) => {
|
||||
if (appStore.layout === 'mix') {
|
||||
appStore.menuCollapse = collapsed
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
71
src/layout/components/MenuFoldBtn.vue
Normal file
71
src/layout/components/MenuFoldBtn.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<a-button size="mini" class="gi_hover_btn menu-fold-btn" @click="onClick">
|
||||
<template #icon>
|
||||
<icon-menu-fold v-if="!appStore.menuCollapse" :size="18" :stroke-width="3" />
|
||||
<icon-menu-unfold v-else :size="18" :stroke-width="3" />
|
||||
</template>
|
||||
</a-button>
|
||||
|
||||
<div
|
||||
class="drawer"
|
||||
:class="{ 'app-menu-dark': appStore.menuDark }"
|
||||
:style="appStore.menuDark ? appStore.themeCSSVar : undefined"
|
||||
>
|
||||
<a-drawer
|
||||
v-model:visible="visible"
|
||||
placement="left"
|
||||
:header="false"
|
||||
:footer="false"
|
||||
:render-to-body="false"
|
||||
:drawer-style="{
|
||||
'border-right': '1px solid var(--color-border-2)',
|
||||
'box-sizing': 'border-box',
|
||||
'background-color': 'var(--color-bg-1)'
|
||||
}"
|
||||
>
|
||||
<Logo :collapsed="false"></Logo>
|
||||
<Menu class="menu w-full" @menu-item-click-after="visible = false"></Menu>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from '@/stores'
|
||||
import Logo from '@/layout/components/Logo.vue'
|
||||
import Menu from '@/layout/components/Menu/index.vue'
|
||||
import { useDevice } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'MenuFoldBtn' })
|
||||
const appStore = useAppStore()
|
||||
const { isDesktop } = useDevice()
|
||||
const visible = ref(false)
|
||||
|
||||
const onClick = () => {
|
||||
if (isDesktop.value) {
|
||||
appStore.setMenuCollapse(!appStore.menuCollapse)
|
||||
} else {
|
||||
visible.value = !visible.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-fold-btn {
|
||||
background-color: var(--color-secondary-hover) !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
.menu {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background-color: inherit;
|
||||
}
|
||||
:deep(.arco-drawer-body) {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
90
src/layout/components/Tabs/MagicIcon.vue
Normal file
90
src/layout/components/Tabs/MagicIcon.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<span class="gi-more-icon-wrap">
|
||||
<span class="gi-more-icon">
|
||||
<i class="block block-top"></i>
|
||||
<i class="block block-bottom"></i>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gi-more-icon-wrap {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: -6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gi-more-icon {
|
||||
display: inline-block;
|
||||
color: var(--color-text-2);
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease-out;
|
||||
|
||||
.block {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 14px;
|
||||
height: 6px;
|
||||
|
||||
// &.block-top:before {
|
||||
// transition: transform 0.3s ease-out 0.3s;
|
||||
// }
|
||||
&.block-bottom {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.block:before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
content: '';
|
||||
background: var(--color-text-3);
|
||||
}
|
||||
|
||||
.block:after {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
content: '';
|
||||
background: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gi-more-icon-wrap:hover .gi-more-icon .block:first-child::before,
|
||||
.arco-dropdown-open .gi-more-icon .block:first-child::before {
|
||||
transform: rotate(45deg);
|
||||
background: rgb(var(--primary-3));
|
||||
}
|
||||
|
||||
.gi-more-icon-wrap:hover .gi-more-icon .block:before,
|
||||
.arco-dropdown-open .gi-more-icon .block:before {
|
||||
background: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
.gi-more-icon-wrap:hover .gi-more-icon .block:after,
|
||||
.arco-dropdown-open .gi-more-icon .block:after {
|
||||
background: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
.gi-more-icon-wrap:hover .gi-more-icon,
|
||||
.arco-dropdown-open .gi-more-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
</style>
|
||||
110
src/layout/components/Tabs/index.vue
Normal file
110
src/layout/components/Tabs/index.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="tabs" v-if="appStore.tab">
|
||||
<a-tabs
|
||||
editable
|
||||
hide-content
|
||||
size="medium"
|
||||
:type="appStore.tabMode"
|
||||
:active-key="route.path"
|
||||
@tab-click="(key) => handleTabClick(key as string)"
|
||||
@delete="tabsStore.closeCurrent"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="item of tabsStore.tagList"
|
||||
:key="item.path"
|
||||
:title="(item.meta?.title as string)"
|
||||
:closable="Boolean(!item.meta?.affix)"
|
||||
>
|
||||
</a-tab-pane>
|
||||
<template #extra>
|
||||
<a-dropdown trigger="hover">
|
||||
<MagicIcon class="gi_mr"></MagicIcon>
|
||||
<template #content>
|
||||
<a-doption @click="tabsStore.closeCurrent(route.path)">
|
||||
<template #icon><icon-close /></template>
|
||||
<template #default>关闭当前</template>
|
||||
</a-doption>
|
||||
<a-doption @click="tabsStore.closeOther(route.path)">
|
||||
<template #icon><icon-eraser /></template>
|
||||
<template #default>关闭其他</template>
|
||||
</a-doption>
|
||||
<a-doption @click="tabsStore.closeAll">
|
||||
<template #icon><icon-minus /></template>
|
||||
<template #default>关闭全部</template>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { useTabsStore, useAppStore } from '@/stores'
|
||||
import MagicIcon from './MagicIcon.vue'
|
||||
|
||||
defineOptions({ name: 'Tabs' })
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const tabsStore = useTabsStore()
|
||||
|
||||
// 重置, 同时把 affix: true 的路由筛选出来
|
||||
tabsStore.reset()
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
handleRouteChange()
|
||||
}
|
||||
)
|
||||
|
||||
// 路由发生改变触发
|
||||
const handleRouteChange = () => {
|
||||
const item = { ...route } as unknown as RouteRecordRaw
|
||||
tabsStore.addTagItem(item)
|
||||
tabsStore.addCacheItem(item)
|
||||
// console.log('路由对象', toRaw(item))
|
||||
// console.log('tagList', toRaw(tabsStore.tagList))
|
||||
// console.log('cacheList', toRaw(tabsStore.cacheList))
|
||||
}
|
||||
handleRouteChange()
|
||||
|
||||
// 点击页签
|
||||
const handleTabClick = (key: string) => {
|
||||
router.push({ path: key })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-tabs-nav-tab) {
|
||||
.arco-tabs-tab {
|
||||
border-bottom-color: transparent !important;
|
||||
svg {
|
||||
width: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
&:hover {
|
||||
svg {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.arco-tabs-nav-tab-scroll) {
|
||||
.arco-tabs-tab:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-dropdown-option-icon) {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
padding-top: 5px;
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
</style>
|
||||
15
src/layout/index.vue
Normal file
15
src/layout/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<LayoutMix v-if="appStore.layout === 'mix'"></LayoutMix>
|
||||
<LayoutDefault v-else></LayoutDefault>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LayoutDefault from './LayoutDefault.vue'
|
||||
import LayoutMix from './LayoutMix.vue'
|
||||
import { useAppStore } from '@/stores'
|
||||
|
||||
defineOptions({ name: 'Layout' })
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
Reference in New Issue
Block a user